Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 15 Apr 2015 15:10:44 -0400
changeset 239345 24ccca4707eb8ff1576620c688c7e3db5691228d
parent 239304 fdaea6d81a14e26587d5f5670f4a9c87fd083808 (current diff)
parent 239344 404ab03433dbead90ba33be2c81b07263ace51b9 (diff)
child 239346 53ceefb0e1c80ded0f3c154e26b96b48c8821210
child 239379 96113a9fa2db96f1d3936b7148cf89f7bf0e8b95
child 239392 dfd7c7d0fa74ab33269291312c32f67d43565237
push id28589
push userryanvm@gmail.com
push dateWed, 15 Apr 2015 19:13:10 +0000
treeherdermozilla-central@24ccca4707eb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge IGNORE IDL CHANGES
browser/devtools/inspector/test/browser_inspector_highlight_after_transition.js
browser/devtools/inspector/test/doc_frame_script.js
--- a/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
+++ b/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
@@ -104,17 +104,17 @@ add_task(function* setup() {
 
     ok(pluginDumpFile.exists(), "Found minidump");
     ok(extraFile.exists(), "Found extra file");
 
     pluginDumpFile.remove(false);
     extraFile.remove(false);
   };
 
-  Services.obs.addObserver(crashObserver, "plugin-crashed");
+  Services.obs.addObserver(crashObserver, "plugin-crashed", false);
   // plugins.testmode will make BrowserPlugins:Test:ClearCrashData work.
   Services.prefs.setBoolPref("plugins.testmode", true);
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("plugins.testmode");
     Services.obs.removeObserver(crashObserver, "plugin-crashed");
   });
 });
 
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1039,16 +1039,24 @@ file, You can obtain one at http://mozil
                 class="search-panel-header search-panel-current-input">
         <xul:label anonid="searchbar-oneoffheader-search" value="&searchWithHeader.label;"/>
         <xul:hbox anonid="search-panel-searchforwith"
                   class="search-panel-current-input">
           <xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
           <xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
           <xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
         </xul:hbox>
+        <xul:hbox anonid="search-panel-searchonengine"
+                  class="search-panel-current-input">
+          <xul:label anonid="searchbar-oneoffheader-beforeengine" value="&search.label;"/>
+          <xul:label anonid="searchbar-oneoffheader-engine" flex="1" crop="end"
+                     class="search-panel-input-value"/>
+          <xul:label anonid="searchbar-oneoffheader-afterengine" flex="10000"
+                     value="&searchAfter.label;"/>
+        </xul:hbox>
       </xul:deck>
       <xul:description anonid="search-panel-one-offs"
                        role="group"
                        class="search-panel-one-offs"/>
       <xul:vbox anonid="add-engines"/>
       <xul:button anonid="search-settings"
                   oncommand="BrowserUITelemetry.countSearchSettingsEvent('searchbar');openPreferences('paneSearch')"
                   class="search-setting-button search-panel-header"
@@ -1135,29 +1143,34 @@ file, You can obtain one at http://mozil
                                                   "search-panel-one-offs-header");
         let list = document.getAnonymousElementByAttribute(this, "anonid",
                                                            "search-panel-one-offs");
         let textbox = searchbar.textbox;
         let self = this;
         let inputHandler = function() {
           headerSearchText.setAttribute("value", textbox.value);
           let groupText;
+          let isOneOffSelected =
+            this.selectedButton &&
+            this.selectedButton.classList.contains("searchbar-engine-one-off-item");
           if (textbox.value) {
             self.removeAttribute("showonlysettings");
             groupText = headerSearchText.previousSibling.value +
                         '"' + headerSearchText.value + '"' +
                         headerSearchText.nextSibling.value;
-            headerPanel.selectedIndex = 1;
+            if (!isOneOffSelected)
+              headerPanel.selectedIndex = 1;
           }
           else {
             let noSearchHeader =
               document.getAnonymousElementByAttribute(self, "anonid",
                                                       "searchbar-oneoffheader-search");
             groupText = noSearchHeader.value;
-            headerPanel.selectedIndex = 0;
+            if (!isOneOffSelected)
+              headerPanel.selectedIndex = 0;
           }
           list.setAttribute("aria-label", groupText);
         };
         textbox.addEventListener("input", inputHandler);
         this.addEventListener("popuphiding", function hiding() {
           textbox.removeEventListener("input", inputHandler);
           this.removeEventListener("popuphiding", hiding);
         });
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -26,17 +26,17 @@ struct RedirEntry {
 /*
   Entries which do not have URI_SAFE_FOR_UNTRUSTED_CONTENT will run with chrome
   privileges. This is potentially dangerous. Please use
   URI_SAFE_FOR_UNTRUSTED_CONTENT in the third argument to each map item below
   unless your about: page really needs chrome privileges. Security review is
   required before adding new map entries without
   URI_SAFE_FOR_UNTRUSTED_CONTENT.  Also note, however, that adding
   URI_SAFE_FOR_UNTRUSTED_CONTENT will allow random web sites to link to that
-  URI.  Perhaps we should separate the two concepts out...
+  URI.  If you want to prevent this, add MAKE_UNLINKABLE as well.
  */
 static RedirEntry kRedirMap[] = {
 #ifdef MOZ_SAFE_BROWSING
   { "blocked", "chrome://browser/content/blockedSite.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::URI_CAN_LOAD_IN_CHILD |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
@@ -117,16 +117,17 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::HIDE_FROM_ABOUTABOUT |
     nsIAboutModule::ENABLE_INDEXED_DB,
     // Shares an IndexedDB origin with about:loopconversation.
     "loopconversation" },
   { "reader", "chrome://global/content/reader/aboutReader.html",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
+    nsIAboutModule::MAKE_UNLINKABLE |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
 };
 static const int kRedirTotal = ArrayLength(kRedirMap);
 
 static nsAutoCString
 GetAboutModuleName(nsIURI *aURI)
 {
   nsAutoCString path;
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -1027,28 +1027,54 @@ body[dir=rtl] .share-service-dropdown .s
 
 .standalone .room-conversation-wrapper .room-inner-info-area a.btn {
   padding: .5em 3em .3em 3em;
   border-radius: 3px;
   font-weight: normal;
   max-width: 400px;
 }
 
-.standalone .room-conversation h2.room-name,
-.standalone .room-conversation h2.room-info-failure {
+.standalone-room-info {
   position: absolute;
-  display: inline-block;
+  display: block;
   top: 0;
   right: 10px;
   /* 20px is 10px for left and right margins. */
   width: calc(25% - 20px);
-  color: #fff;
   z-index: 2000000;
   font-size: 1.2em;
   padding: .4em;
+  height: 100%;
+}
+
+.standalone-room-info > h2 {
+  color: #fff;
+}
+
+.standalone-context-url {
+  color: #fff;
+  /* Try and keep clear of local video */
+  height: 40%;
+}
+
+.standalone-context-url.screen-share-active {
+  /* Try and keep clear of remote video when screensharing */
+  height: 15%;
+}
+
+.standalone-context-url > img {
+  margin: 1em auto;
+  max-width: 50%;
+  /* allows 20% for the description wrapper plus the margins */
+  max-height: calc(80% - 2em);
+}
+
+.standalone-context-url-description-wrapper {
+  /* So that we can use max-height for the image */
+  height: 20%;
 }
 
 .standalone .room-conversation .media {
   background: #000;
 }
 
 .standalone .room-conversation .video_wrapper.remote_wrapper {
   background-color: #4e4e4e;
@@ -1066,16 +1092,30 @@ body[dir=rtl] .share-service-dropdown .s
 
 .standalone .room-conversation-wrapper .ended-conversation {
   position: relative;
   height: auto;
 }
 
 
 @media screen and (max-width:640px) {
+  .standalone-room-info {
+    /* This isn't perfect, we just center the heading for now. Bug 1141493
+       should fix this. */
+    position: absolute;
+    width: 100%;
+    right: 0px;
+  }
+
+  .standalone-context-url {
+    /* XXX We haven't got UX for standalone yet, so temporarily not displaying
+       on narrow window widths. See bug 1153827. */
+    display: none;
+  }
+
   /* Rooms specific responsive styling */
   .standalone .room-conversation {
     background: #000;
   }
   .room-conversation-wrapper header {
     width: 100%;
   }
   .standalone .room-conversation-wrapper .room-inner-info-area {
@@ -1086,16 +1126,20 @@ body[dir=rtl] .share-service-dropdown .s
   }
   .standalone .room-conversation-wrapper .video-layout-wrapper {
     /* 50px: header's height; 25px: footer's height */
     height: calc(100% - 50px - 25px);
   }
   .standalone .room-conversation .video_wrapper.remote_wrapper {
     width: 100%;
   }
+  .standalone .room-conversation .video_wrapper.remote_wrapper.not-joined {
+    width: 100%;
+  }
+
   .standalone .conversation-toolbar {
     height: 38px;
     padding: 8px;
   }
   .standalone .focus-stream {
     /* Set at maximum height, minus height of conversation toolbar */
     height: 100%;
   }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -414,16 +414,18 @@ loop.shared.actions = (function() {
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
       // context: Object - Optional.
       // roomName: String - Optional.
       roomOwner: String,
       roomUrl: String
+      // urls: Array - Optional.
+      // See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
     }),
 
     /**
      * Updates the Social API information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     UpdateSocialShareInfo: Action.define("updateSocialShareInfo", {
       socialShareButtonAvailable: Boolean,
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -75,20 +75,24 @@ loop.store.ActiveRoomStore = (function()
         // session. 'Used' means at least one call has been placed
         // with it. Entering and leaving the room without seeing
         // anyone is not considered as 'used'
         used: false,
         localVideoDimensions: {},
         remoteVideoDimensions: {},
         screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
         receivingScreenShare: false,
+        // Any urls (aka context) associated with the room.
+        roomContextUrls: null,
         // The roomCryptoKey to decode the context data if necessary.
         roomCryptoKey: null,
         // Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
         roomInfoFailure: null,
+        // The name of the room.
+        roomName: null,
         // Social API state.
         socialShareButtonAvailable: false,
         socialShareProviders: null
       };
     },
 
     /**
      * Handles a room failure.
@@ -266,16 +270,17 @@ loop.store.ActiveRoomStore = (function()
         }
 
         var dispatcher = this.dispatcher;
 
         crypto.decryptBytes(roomCryptoKey, result.context.value)
               .then(function(decryptedResult) {
           var realResult = JSON.parse(decryptedResult);
 
+          roomInfoData.urls = realResult.urls;
           roomInfoData.roomName = realResult.roomName;
 
           dispatcher.dispatch(roomInfoData);
         }, function(err) {
           roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
           dispatcher.dispatch(roomInfoData);
         });
       }.bind(this));
@@ -315,16 +320,17 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
      *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       this.setStoreState({
+        roomContextUrls: actionData.urls,
         roomInfoFailure: actionData.roomInfoFailure,
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
         roomUrl: actionData.roomUrl
       });
     },
 
     /**
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -194,35 +194,79 @@ loop.standaloneRoomViews = (function(moz
         React.createElement("footer", null, 
           React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}}), 
           React.createElement("div", {className: "footer-logo"})
         )
       );
     }
   });
 
+  var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
+    propTypes: {
+      receivingScreenShare: React.PropTypes.bool,
+      roomContextUrl: React.PropTypes.object
+    },
+
+    render: function() {
+      if (!this.props.roomContextUrl ||
+          !this.props.roomContextUrl.location) {
+        return null;
+      }
+
+      var location = this.props.roomContextUrl.location;
+
+      var cx = React.addons.classSet;
+
+      var classes = cx({
+        "standalone-context-url": true,
+        "screen-share-active": this.props.receivingScreenShare
+      });
+
+      return (
+        React.createElement("div", {className: classes}, 
+            React.createElement("img", {src: this.props.roomContextUrl.thumbnail}), 
+          React.createElement("div", {className: "standalone-context-url-description-wrapper"}, 
+            this.props.roomContextUrl.description, 
+            React.createElement("br", null), React.createElement("a", {href: location}, location)
+          )
+        )
+      );
+    }
+  });
+
   var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
     propTypes: {
+      receivingScreenShare: React.PropTypes.bool.isRequired,
+      roomContextUrls: React.PropTypes.array,
       roomName: React.PropTypes.string,
       roomInfoFailure: React.PropTypes.string
     },
 
     render: function() {
       if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
         return (React.createElement("h2", {className: "room-info-failure"}, 
           mozL10n.get("room_information_failure_unsupported_browser")
         ));
       } else if (this.props.roomInfoFailure) {
         return (React.createElement("h2", {className: "room-info-failure"}, 
           mozL10n.get("room_information_failure_not_available")
         ));
       }
 
+      // We only support one item in the context Urls array for now.
+      var roomContextUrl = (this.props.roomContextUrls &&
+                            this.props.roomContextUrls.length > 0) ?
+                            this.props.roomContextUrls[0] : null;
       return (
-        React.createElement("h2", {className: "room-name"}, this.props.roomName)
+        React.createElement("div", {className: "standalone-room-info"}, 
+          React.createElement("h2", {className: "room-name"}, this.props.roomName), 
+          React.createElement(StandaloneRoomContextItem, {
+            receivingScreenShare: this.props.receivingScreenShare, 
+            roomContextUrl: roomContextUrl})
+        )
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
@@ -477,18 +521,21 @@ loop.standaloneRoomViews = (function(moz
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
                                   isFirefox: this.props.isFirefox, 
                                   activeRoomStore: this.props.activeRoomStore, 
                                   roomUsed: this.state.used}), 
           React.createElement("div", {className: "video-layout-wrapper"}, 
             React.createElement("div", {className: "conversation room-conversation"}, 
-              React.createElement(StandaloneRoomContextView, {roomName: this.state.roomName, 
-                                         roomInfoFailure: this.state.roomInfoFailure}), 
+              React.createElement(StandaloneRoomContextView, {
+                receivingScreenShare: this.state.receivingScreenShare, 
+                roomContextUrls: this.state.roomContextUrls, 
+                roomName: this.state.roomName, 
+                roomInfoFailure: this.state.roomInfoFailure}), 
               React.createElement("div", {className: "media nested"}, 
                 React.createElement("span", {className: "self-view-hidden-message"}, 
                   mozL10n.get("self_view_hidden_message")
                 ), 
                 React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
                   React.createElement("div", {className: remoteStreamClasses}), 
                   React.createElement("div", {className: screenShareStreamClasses})
                 ), 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -194,35 +194,79 @@ loop.standaloneRoomViews = (function(moz
         <footer>
           <p dangerouslySetInnerHTML={{__html: this._getContent()}}></p>
           <div className="footer-logo" />
         </footer>
       );
     }
   });
 
+  var StandaloneRoomContextItem = React.createClass({
+    propTypes: {
+      receivingScreenShare: React.PropTypes.bool,
+      roomContextUrl: React.PropTypes.object
+    },
+
+    render: function() {
+      if (!this.props.roomContextUrl ||
+          !this.props.roomContextUrl.location) {
+        return null;
+      }
+
+      var location = this.props.roomContextUrl.location;
+
+      var cx = React.addons.classSet;
+
+      var classes = cx({
+        "standalone-context-url": true,
+        "screen-share-active": this.props.receivingScreenShare
+      });
+
+      return (
+        <div className={classes}>
+            <img src={this.props.roomContextUrl.thumbnail} />
+          <div className="standalone-context-url-description-wrapper">
+            {this.props.roomContextUrl.description}
+            <br /><a href={location}>{location}</a>
+          </div>
+        </div>
+      );
+    }
+  });
+
   var StandaloneRoomContextView = React.createClass({
     propTypes: {
+      receivingScreenShare: React.PropTypes.bool.isRequired,
+      roomContextUrls: React.PropTypes.array,
       roomName: React.PropTypes.string,
       roomInfoFailure: React.PropTypes.string
     },
 
     render: function() {
       if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
         return (<h2 className="room-info-failure">
           {mozL10n.get("room_information_failure_unsupported_browser")}
         </h2>);
       } else if (this.props.roomInfoFailure) {
         return (<h2 className="room-info-failure">
           {mozL10n.get("room_information_failure_not_available")}
         </h2>);
       }
 
+      // We only support one item in the context Urls array for now.
+      var roomContextUrl = (this.props.roomContextUrls &&
+                            this.props.roomContextUrls.length > 0) ?
+                            this.props.roomContextUrls[0] : null;
       return (
-        <h2 className="room-name">{this.props.roomName}</h2>
+        <div className="standalone-room-info">
+          <h2 className="room-name">{this.props.roomName}</h2>
+          <StandaloneRoomContextItem
+            receivingScreenShare={this.props.receivingScreenShare}
+            roomContextUrl={roomContextUrl} />
+        </div>
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
@@ -477,18 +521,21 @@ loop.standaloneRoomViews = (function(moz
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
                                   isFirefox={this.props.isFirefox}
                                   activeRoomStore={this.props.activeRoomStore}
                                   roomUsed={this.state.used} />
           <div className="video-layout-wrapper">
             <div className="conversation room-conversation">
-              <StandaloneRoomContextView roomName={this.state.roomName}
-                                         roomInfoFailure={this.state.roomInfoFailure} />
+              <StandaloneRoomContextView
+                receivingScreenShare={this.state.receivingScreenShare}
+                roomContextUrls={this.state.roomContextUrls}
+                roomName={this.state.roomName}
+                roomInfoFailure={this.state.roomInfoFailure} />
               <div className="media nested">
                 <span className="self-view-hidden-message">
                   {mozL10n.get("self_view_hidden_message")}
                 </span>
                 <div className="video_wrapper remote_wrapper">
                   <div className={remoteStreamClasses}></div>
                   <div className={screenShareStreamClasses}></div>
                 </div>
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -452,36 +452,43 @@ describe("loop.store.ActiveRoomStore", f
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo(_.extend({
             roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED
           }, expectedDetails)));
       });
 
-      it("should dispatch UpdateRoomInfo message with the room name if decryption was successful", function() {
+      it("should dispatch UpdateRoomInfo message with the context if decryption was successful", function() {
         fetchServerAction.cryptoKey = "fakeKey";
 
+        var roomContext = {
+          roomName: "The wonderful Loopy room",
+          urls: [{
+            description: "An invalid page",
+            location: "http://invalid.com",
+            thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+          }]
+        };
+
         // This is a work around to turn promise into a sync action to make handling test failures
         // easier.
         sandbox.stub(loop.crypto, "decryptBytes", function() {
           return {
             then: function(resolve, reject) {
-              resolve(JSON.stringify({roomName: "The wonderful Loopy room"}));
+              resolve(JSON.stringify(roomContext));
             }
           };
         });
 
         store.fetchServerData(fetchServerAction);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.UpdateRoomInfo(_.extend({
-            roomName: "The wonderful Loopy room"
-          }, expectedDetails)));
+          new sharedActions.UpdateRoomInfo(_.extend(roomContext, expectedDetails)));
       });
     });
   });
 
   describe("#feedbackComplete", function() {
     it("should reset the room store state", function() {
       var initialState = store.getInitialStoreState();
       store.setStoreState({
@@ -557,27 +564,33 @@ describe("loop.store.ActiveRoomStore", f
 
   describe("#updateRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
         roomOwner: "Me",
-        roomUrl: "http://invalid"
+        roomUrl: "http://invalid",
+        urls: [{
+          description: "fake site",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
       };
     });
 
     it("should save the room information", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
 
       var state = store.getStoreState();
       expect(state.roomName).eql(fakeRoomInfo.roomName);
       expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
       expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
+      expect(state.roomContextUrls).eql(fakeRoomInfo.urls);
     });
   });
 
   describe("#updateSocialShareInfo", function() {
     var fakeSocialShareInfo;
 
     beforeEach(function() {
       fakeSocialShareInfo = {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -39,25 +39,27 @@ describe("loop.standaloneRoomViews", fun
     sandbox.restore();
   });
 
   describe("StandaloneRoomContextView", function() {
     beforeEach(function() {
       sandbox.stub(navigator.mozL10n, "get").returnsArg(0);
     });
 
-    function mountTestComponent(props) {
+    function mountTestComponent(extraProps) {
+      var props = _.extend({ receivingScreenShare: false }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomContextView, props));
     }
 
     it("should display the room name if no failures are known", function() {
       var view = mountTestComponent({
-        roomName: "Mike's room"
+        roomName: "Mike's room",
+        receivingScreenShare: false
       });
 
       expect(view.getDOMNode().textContent).eql("Mike's room");
     });
 
     it("should display an unsupported browser message if crypto is unsupported", function() {
       var view = mountTestComponent({
         roomName: "Mark's room",
@@ -70,16 +72,37 @@ describe("loop.standaloneRoomViews", fun
     it("should display a general error message for any other failure", function() {
       var view = mountTestComponent({
         roomName: "Mark's room",
         roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA
       });
 
       expect(view.getDOMNode().textContent).match(/not_available/);
     });
+
+    it("should display context information if a url is supplied", function() {
+      var view = mountTestComponent({
+        roomName: "Mike's room",
+        roomContextUrls: [{
+          description: "Mark's super page",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
+      });
+
+      expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null);
+    });
+
+    it("should not display context information if no urls are supplied", function() {
+      var view = mountTestComponent({
+        roomName: "Mike's room"
+      });
+
+      expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
+    });
   });
 
   describe("StandaloneRoomView", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomView, {
             dispatcher: dispatcher,
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -1016,24 +1016,39 @@
       </method>
 
       <field name="_selectedButton"/>
       <property name="selectedButton" onget="return this._selectedButton;">
         <setter><![CDATA[
           if (this._selectedButton)
             this._selectedButton.removeAttribute("selected");
 
+          let textbox = document.getBindingParent(this).textbox;
+          let header =
+            document.getAnonymousElementByAttribute(this.popup, "anonid",
+                                                    "search-panel-one-offs-header");
           // Avoid selecting dummy buttons.
           if (val && !val.classList.contains("dummy")) {
             val.setAttribute("selected", "true");
             this._selectedButton = val;
+            if (val.classList.contains("searchbar-engine-one-off-item")) {
+              let headerEngineText =
+                document.getAnonymousElementByAttribute(this.popup, "anonid",
+                                                        "searchbar-oneoffheader-engine");
+              header.selectedIndex = 2;
+              headerEngineText.value = val.engine.name;
+            }
+            else {
+              header.selectedIndex = textbox.value ? 1 : 0;
+            }
             this.setAttribute("aria-activedescendant", val.id);
             return;
           }
 
+          header.selectedIndex = textbox.value ? 1 : 0;
           this.removeAttribute("aria-activedescendant");
           this._selectedButton = null;
         ]]></setter>
       </property>
 
       <method name="getSelectableButtons">
         <parameter name="aCycleEngines"/>
         <body><![CDATA[
--- a/browser/components/search/test/browser.ini
+++ b/browser/components/search/test/browser.ini
@@ -31,16 +31,18 @@ skip-if = e10s # Bug ?????? - Test touch
 [browser_eBay_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_google.js]
 [browser_google_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_healthreport.js]
 [browser_hiddenOneOffs_cleanup.js]
 [browser_hiddenOneOffs_diacritics.js]
+[browser_oneOffHeader.js]
+skip-if = e10s # bug ?????? - Test alters the searchbar textbox value which causes issues with other tests in e10s.
 [browser_private_search_perwindowpb.js]
 skip-if = e10s # Bug ?????? - Test uses load event and checks event.target.
 [browser_yahoo.js]
 [browser_yahoo_behavior.js]
 skip-if = e10s # Bug ?????? - some issue with progress listeners [JavaScript Error: "req.originalURI is null" {file: "chrome://mochitests/content/browser/browser/components/search/test/browser_bing_behavior.js" line: 127}]
 [browser_abouthome_behavior.js]
 skip-if = e10s || true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
 [browser_searchbar_openpopup.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/search/test/browser_oneOffHeader.js
@@ -0,0 +1,141 @@
+/* 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/. */
+// Tests that keyboard navigation in the search panel works as designed.
+
+const isMac = ("nsILocalFileMac" in Ci);
+
+const searchbar = document.getElementById("searchbar");
+const textbox = searchbar._textbox;
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const searchIcon = document.getAnonymousElementByAttribute(searchbar, "anonid",
+                                                           "searchbar-search-button");
+const searchSettings =
+  document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                          "search-settings");
+let header =
+  document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                          "search-panel-one-offs-header");
+function getHeaderText() {
+  let headerChild = header.selectedPanel;
+  while (headerChild.hasChildNodes()) {
+    headerChild = headerChild.firstChild;
+  }
+  let headerStrings = [];
+  for (let label = headerChild; label; label = label.nextSibling) {
+    headerStrings.push(label.value);
+  }
+  return headerStrings.join("");
+}
+
+// Get an array of the one-off buttons.
+function getOneOffs() {
+  let oneOffs = [];
+  let oneOff =
+    document.getAnonymousElementByAttribute(searchPopup, "anonid",
+                                            "search-panel-one-offs");
+  for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+    if (oneOff.classList.contains("dummy"))
+      break;
+    oneOffs.push(oneOff);
+  }
+
+  return oneOffs;
+}
+
+const msg = isMac ? 5 : 1;
+const utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
+const scale = utils.screenPixelsPerCSSPixel;
+function* synthesizeNativeMouseMove(aElement) {
+  let rect = aElement.getBoundingClientRect();
+  let win = aElement.ownerDocument.defaultView;
+  let x = win.mozInnerScreenX + (rect.left + rect.right) / 2;
+  let y = win.mozInnerScreenY + (rect.top + rect.bottom) / 2;
+
+  // Wait for the mouseup event to occur before continuing.
+  return new Promise((resolve, reject) => {
+    function eventOccurred(e)
+    {
+      aElement.removeEventListener("mouseover", eventOccurred, true);
+      resolve();
+    }
+
+    aElement.addEventListener("mouseover", eventOccurred, true);
+
+    utils.sendNativeMouseEvent(x * scale, y * scale, msg, 0, null);
+  });
+}
+
+
+add_task(function* init() {
+  yield promiseNewEngine("testEngine.xml");
+});
+
+add_task(function* test_notext() {
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+  yield promise;
+
+  is(header.getAttribute("selectedIndex"), 0,
+     "Header has the correct index selected with no search terms.");
+
+  is(getHeaderText(), "Search with:",
+     "Search header string is correct when no search terms have been entered");
+
+  yield synthesizeNativeMouseMove(searchSettings);
+  is(header.getAttribute("selectedIndex"), 0,
+     "Header has the correct index when no search terms have been entered and the Change Search Settings button is selected.");
+  is(getHeaderText(), "Search with:",
+     "Header has the correct text when no search terms have been entered and the Change Search Settings button is selected.");
+
+  let buttons = getOneOffs();
+  yield synthesizeNativeMouseMove(buttons[0]);
+  is(header.getAttribute("selectedIndex"), 2,
+     "Header has the correct index selected when a search engine has been selected");
+  is(getHeaderText(), "Search " + buttons[0].engine.name,
+     "Is the header text correct when a search engine is selected and no terms have been entered.");
+
+  promise = promiseEvent(searchPopup, "popuphidden");
+  info("Closing search panel");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+});
+
+add_task(function* test_text() {
+  textbox.value = "foo";
+  registerCleanupFunction(() => {
+    textbox.value = "";
+  });
+
+  let promise = promiseEvent(searchPopup, "popupshown");
+  info("Opening search panel");
+  SimpleTest.executeSoon(() => {
+    EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+  });
+  yield promise;
+
+  is(header.getAttribute("selectedIndex"), 1,
+     "Header has the correct index selected with a search term.");
+  is(getHeaderText(), "Search for foo with:",
+     "Search header string is correct when a search term has been entered");
+
+  let buttons = getOneOffs();
+  yield synthesizeNativeMouseMove(buttons[0]);
+  is(header.getAttribute("selectedIndex"), 2,
+     "Header has the correct index selected when a search engine has been selected");
+  is(getHeaderText(), "Search " + buttons[0].engine.name,
+     "Is the header text correct when search terms are entered after a search engine has been selected.");
+
+  yield synthesizeNativeMouseMove(searchSettings);
+  is(header.getAttribute("selectedIndex"), 1,
+     "Header has the correct index selected when search terms have been entered and the Change Search Settings button is selected.");
+  is(getHeaderText(), "Search for foo with:",
+     "Header has the correct text when search terms have been entered and the Change Search Settings button is selected.");
+
+  promise = promiseEvent(searchPopup, "popuphidden");
+  info("Closing search panel");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield promise;
+});
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -276,14 +276,18 @@ if (typeof Mozilla == 'undefined') {
 	};
 
 	Mozilla.UITour.openSearchPanel = function(callback) {
 		_sendEvent('openSearchPanel', {
 			callbackID: _waitForCallback(callback)
 		});
 	};
 
+	Mozilla.UITour.forceShowReaderIcon = function() {
+		_sendEvent('forceShowReaderIcon');
+	};
+
 })();
 
 // Make this library Require-able.
 if (typeof module !== 'undefined' && module.exports) {
   module.exports = Mozilla.UITour;
 }
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -22,23 +22,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Metrics",
   "resource://gre/modules/Metrics.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
+  "resource:///modules/ReaderParent.jsm");
 
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL      = "browser.uitour.loglevel";
 const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
 
 const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([
   "endUrlbarCapture",
+  "forceShowReaderIcon",
   "getConfiguration",
   "getTreatmentTag",
   "hideHighlight",
   "hideInfo",
   "hideMenu",
   "ping",
   "registerPageID",
   "setConfiguration",
@@ -672,16 +675,21 @@ this.UITour = {
         break;
       }
 
       case "ping": {
         if (typeof data.callbackID == "string")
           this.sendPageCallback(messageManager, data.callbackID);
         break;
       }
+
+      case "forceShowReaderIcon": {
+        ReaderParent.forceShowReaderIcon(browser);
+        break;
+      }
     }
 
     if (!this.tourBrowsersByWindow.has(window)) {
       this.tourBrowsersByWindow.set(window, new Set());
     }
     this.tourBrowsersByWindow.get(window).add(browser);
 
     Services.obs.addObserver(this, "message-manager-close", false);
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -13,16 +13,18 @@ skip-if = e10s # Bug 941428 - UITour.jsm
 # [browser_UITour3.js] Bug 1113038
 # skip-if = os == "linux" || e10s # Linux: Bug 986760, Bug 989101; e10s: Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_availableTargets.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_detach_tab.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_annotation_size_attributes.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
+[browser_UITour_forceReaderMode.js]
+skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
 [browser_UITour_heartbeat.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
 [browser_UITour_loop.js]
 skip-if = os == "linux" || e10s # Bug 941428 - UITour.jsm not e10s friendly.
 [browser_UITour_modalDialog.js]
 skip-if = os != "mac" || e10s # modal dialog disabling only working on OS X.Bug 941428 - UITour.jsm not e10s friendly
 [browser_UITour_observe.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
new file mode 100644
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_forceReaderMode.js
@@ -0,0 +1,23 @@
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  UITourTest();
+}
+
+let tests = [
+  taskify(function*() {
+    ok(!gBrowser.selectedBrowser.isArticle, "Should not be an article when we start");
+    ok(document.getElementById("reader-mode-button").hidden, "Button should be hidden.");
+    gContentAPI.forceShowReaderIcon();
+    yield waitForConditionPromise(() => gBrowser.selectedBrowser.isArticle);
+    ok(gBrowser.selectedBrowser.isArticle, "Should suddenly be an article.");
+    ok(!document.getElementById("reader-mode-button").hidden, "Button should now be visible.");
+  })
+];
+
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -420,48 +420,57 @@ SelectorSearch.prototype = {
         break;
     }
     this.emit("processing-done");
   },
 
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
-  _showPopup: function(aList, aFirstPart) {
+  _showPopup: function(aList, aFirstPart, aState) {
     let total = 0;
     let query = this.searchBox.value;
-    let toLowerCase = false;
     let items = [];
-    // In case of tagNames, change the case to small.
-    if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
-      toLowerCase = true;
-    }
-    for (let [value, count] of aList) {
+
+    for (let [value, count, state] of aList) {
       // for cases like 'div ' or 'div >' or 'div+'
       if (query.match(/[\s>+]$/)) {
         value = query + value;
       }
       // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
       else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) {
         let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
       }
       // for cases like 'div.class' or '#foo.bar' and likewise
       else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
         let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
       }
+
       let item = {
         preLabel: query,
         label: value,
         count: count
       };
-      if (toLowerCase) {
+
+      // In case of tagNames, change te case to small
+      if (value.match(/.*[\.#][^\.#]{0,}$/) == null) {
         item.label = value.toLowerCase();
       }
+
+      // In case the query's state is tag and the item's state is id or class
+      // adjust the preLabel
+      if (aState === this.States.TAG && state === this.States.CLASS) {
+        item.preLabel = "." + item.preLabel;
+      }
+      if (aState === this.States.TAG && state === this.States.ID) {
+        item.preLabel = "#" + item.preLabel;
+      }
+
       items.unshift(item);
       if (++total > MAX_SUGGESTIONS - 1) {
         break;
       }
     }
     if (total > 0) {
       this.searchPopup.setItems(items);
       this.searchPopup.openPopup(this.searchBox);
@@ -472,48 +481,53 @@ SelectorSearch.prototype = {
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function() {
     let query = this.searchBox.value;
+    let state = this.state;
     let firstPart = "";
-    if (this.state == this.States.TAG) {
+
+    if (state == this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
       firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
       query = query.slice(0, query.length - firstPart.length);
     }
-    else if (this.state == this.States.CLASS) {
+    else if (state == this.States.CLASS) {
       // gets the class that is being completed. For ex. '.foo.b' returns 'b'
       firstPart = query.match(/\.([^\.]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
-    else if (this.state == this.States.ID) {
+    else if (state == this.States.ID) {
       // gets the id that is being completed. For ex. '.foo#b' returns 'b'
       firstPart = query.match(/#([^#]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
     // TODO: implement some caching so that over the wire request is not made
     // everytime.
     if (/[\s+>~]$/.test(query)) {
       query += "*";
     }
+
     this._currentSuggesting = query;
-    return this.walker.getSuggestionsForQuery(query, firstPart, this.state).then(result => {
+    return this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => {
       if (this._currentSuggesting != result.query) {
         // This means that this response is for a previous request and the user
         // as since typed something extra leading to a new request.
         return;
       }
       this._lastToLastValidSearch = this._lastValidSearch;
-      if (this.state == this.States.CLASS) {
+
+      if (state == this.States.CLASS) {
         firstPart = "." + firstPart;
       }
-      else if (this.state == this.States.ID) {
+      else if (state == this.States.ID) {
         firstPart = "#" + firstPart;
       }
-      this._showPopup(result.suggestions, firstPart);
+
+      this._showPopup(result.suggestions, firstPart, state);
     });
   }
 };
--- a/browser/devtools/inspector/test/browser_inspector_search-02.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-02.js
@@ -10,17 +10,21 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 const TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [
+      {label: "div", count: 4},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 4}]
   },
   {
     key: "v",
     suggestions: []
@@ -62,17 +66,21 @@ const TEST_DATA = [
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 4}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [
+      {label: "div", count: 4},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "p",
     suggestions: []
@@ -130,28 +138,29 @@ add_task(function* () {
 
     let command = once(searchBox, "command");
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
     yield command;
 
     info("Waiting for search query to complete");
     yield inspector.searchSuggestions._lastQuery;
 
-    info("Query completed. Performing checks for input '" + searchBox.value + "'");
+    info("Query completed. Performing checks for input '" + searchBox.value +
+      "' - key pressed: " + key);
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
--- a/browser/devtools/inspector/test/browser_inspector_search-03.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-03.js
@@ -10,17 +10,21 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 let TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [
+      {label: "div", count: 2},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 2}]
   },
   {
     key: "v",
     suggestions: []
@@ -45,17 +49,21 @@ let TEST_DATA = [
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 2}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [
+      {label: "div", count: 2},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
@@ -174,21 +182,21 @@ add_task(function* () {
 
     info("Query completed. Performing checks for input '" + searchBox.value + "'");
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
--- a/browser/devtools/inspector/test/browser_inspector_search-04.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-04.js
@@ -13,33 +13,41 @@ const TEST_URL = "data:text/html;charset
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 let TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [
+      {label: "div", count: 5},
+      {label: "#d1", count: 2},
+      {label: "#d2", count: 2}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 5}]
   },
   {
     key: "v",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 5}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [
+      {label: "div", count: 5},
+      {label: "#d1", count: 2},
+      {label: "#d2", count: 2}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
@@ -85,21 +93,21 @@ add_task(function* () {
 
     info("Query completed. Performing checks for input '" + searchBox.value + "'");
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_search-suggests-ids-and-classes.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the selector-search input proposes ids and classes even when . and
+// # is missing, but that this only occurs when the query is one word (no
+// selector combination)
+
+function test()
+{
+  waitForExplicitFinish();
+
+  let inspector, searchBox, state, popup;
+
+  // The various states of the inspector: [key, suggestions array]
+  // [
+  //  what key to press,
+  //  suggestions array with count [
+  //    [suggestion1, count1], [suggestion2] ...
+  //  ] count can be left to represent 1
+  // ]
+  let keyStates = [
+    ["s", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["p", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["a", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["n", []],
+    [" ", [["span div", 1]]],
+    ["d", [["span div", 1]]], // mixed tag/class/id suggestions only work for the first word
+    ["VK_BACK_SPACE", [["span div", 1]]],
+    ["VK_BACK_SPACE", []],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", []],
+    // Test that mixed tags, classes and ids are grouped by types, sorted by
+    // count and alphabetical order
+    ["b", [
+      ["button", 3],
+      ["body", 1],
+      [".bc", 3],
+      [".ba", 1],
+      [".bb", 1],
+      ["#ba", 1],
+      ["#bb", 1],
+      ["#bc", 1]
+    ]],
+  ];
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "data:text/html," +
+                     "<span class='span' id='span'>" +
+                     "  <div class='div' id='div'></div>" +
+                     "</span>" +
+                     "<button class='ba bc' id='bc'></button>" +
+                     "<button class='bb bc' id='bb'></button>" +
+                     "<button class='bc' id='ba'></button>";
+
+  function $(id) {
+    if (id == null) return null;
+    return content.document.getElementById(id);
+  }
+
+  function setupTest()
+  {
+    openInspector(startTest);
+  }
+
+  function startTest(aInspector)
+  {
+    inspector = aInspector;
+
+    searchBox =
+      inspector.panelWin.document.getElementById("inspector-searchbox");
+    popup = inspector.searchSuggestions.searchPopup;
+
+    focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+      searchBox.addEventListener("command", checkState, true);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == keyStates.length) {
+      finishUp();
+      return;
+    }
+
+    let [key, suggestions] = keyStates[index];
+    state = index;
+
+    info("pressing key " + key + " to get suggestions " +
+         JSON.stringify(suggestions));
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState(event) {
+    inspector.searchSuggestions._lastQuery.then(() => {
+      let [key, suggestions] = keyStates[state];
+      let actualSuggestions = popup.getItems();
+      is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
+         "There are expected number of suggestions at " + state + "th step.");
+      actualSuggestions.reverse();
+      for (let i = 0; i < suggestions.length; i++) {
+        is(suggestions[i][0], actualSuggestions[i].label,
+           "The suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+        is(suggestions[i][1] || 1, actualSuggestions[i].count,
+           "The count for suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+      }
+      checkStateAndMoveOn(state + 1);
+    });
+  }
+
+  function finishUp() {
+    searchBox = null;
+    popup = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/layoutview/view.css
+++ b/browser/devtools/layoutview/view.css
@@ -55,16 +55,26 @@ body {
   border-style: solid;
   border-width: 25px;
 }
 
 #borders {
   padding: 25px;
 }
 
+.legend {
+  position: absolute;
+  margin: 5px 6px;
+  z-index: 1;
+}
+
+.legend[data-box="margin"] {
+  color: var(--theme-highlight-blue);
+}
+
 #main > p {
   position: absolute;
   pointer-events: none;
 }
 
 #main > p {
   margin: 0;
   text-align: center;
@@ -164,26 +174,19 @@ body {
 .rotate.left:not(.editing) {
   transform: rotate(-90deg);
 }
 
 .rotate.right:not(.editing) {
   transform: rotate(90deg);
 }
 
-.tooltip {
-  position: absolute;
-  bottom: 0;
-  right: 2px;
-  pointer-events: none;
-}
 
 body.dim > #header > #element-position,
-body.dim > #main > p,
-body.dim > #main > .tooltip {
+body.dim > #main > p {
   visibility: hidden;
 }
 
 @media (max-height: 228px) {
   #header {
     padding-top: 0;
     padding-bottom: 0;
     margin-top: 10px;
--- a/browser/devtools/layoutview/view.js
+++ b/browser/devtools/layoutview/view.js
@@ -495,40 +495,35 @@ LayoutView.prototype = {
     if (classList.contains("left") || classList.contains("right")) {
       let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
       classList.toggle("rotate", force);
     }
   }
 };
 
 let elts;
-let tooltip;
 
 let onmouseover = function(e) {
   let region = e.target.getAttribute("data-box");
-
-  tooltip.textContent = e.target.getAttribute("tooltip");
   this.layoutview.showBoxModel({region});
 
   return false;
 }.bind(window);
 
 let onmouseout = function(e) {
-  tooltip.textContent = "";
   this.layoutview.hideBoxModel();
 
   return false;
 }.bind(window);
 
 window.setPanel = function(panel) {
   this.layoutview = new LayoutView(panel, window);
 
-  // Tooltip mechanism
-  elts = document.querySelectorAll("*[tooltip]");
-  tooltip = document.querySelector(".tooltip");
+  // Box model highlighter mechanism
+  elts = document.querySelectorAll("*[title]");
   for (let i = 0; i < elts.length; i++) {
     let elt = elts[i];
     elt.addEventListener("mouseover", onmouseover, true);
     elt.addEventListener("mouseout", onmouseout, true);
   }
 
   // Mark document as RTL or LTR:
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
--- a/browser/devtools/layoutview/view.xhtml
+++ b/browser/devtools/layoutview/view.xhtml
@@ -25,43 +25,44 @@
   <body class="theme-sidebar devtools-monospace">
 
     <p id="header">
       <span id="element-size"></span><span id="element-position"></span>
     </p>
 
     <div id="main">
 
-      <div id="margins" data-box="margin" tooltip="&margin.tooltip;">
-        <div id="borders" data-box="border" tooltip="&border.tooltip;">
-          <div id="padding" data-box="padding" tooltip="&padding.tooltip;">
-            <div id="content" data-box="content" tooltip="&content.tooltip;">
+      <span class="legend" data-box="margin">&margin.tooltip;</span>
+      <div id="margins" data-box="margin" title="&margin.tooltip;">
+        <span class="legend" data-box="border">&border.tooltip;</span>
+        <div id="borders" data-box="border" title="&border.tooltip;">
+          <span class="legend" data-box="padding">&padding.tooltip;</span>
+          <div id="padding" data-box="padding" title="&padding.tooltip;">
+            <div id="content" data-box="content" title="&content.tooltip;">
             </div>
           </div>
         </div>
       </div>
 
-      <p class="border top"><span data-box="border" class="editable" tooltip="border-top"></span></p>
-      <p class="border right"><span data-box="border" class="editable" tooltip="border-right"></span></p>
-      <p class="border bottom"><span data-box="border" class="editable" tooltip="border-bottom"></span></p>
-      <p class="border left"><span data-box="border" class="editable" tooltip="border-left"></span></p>
+      <p class="border top"><span data-box="border" class="editable" title="border-top"></span></p>
+      <p class="border right"><span data-box="border" class="editable" title="border-right"></span></p>
+      <p class="border bottom"><span data-box="border" class="editable" title="border-bottom"></span></p>
+      <p class="border left"><span data-box="border" class="editable" title="border-left"></span></p>
 
-      <p class="margin top"><span data-box="margin" class="editable" tooltip="margin-top"></span></p>
-      <p class="margin right"><span data-box="margin" class="editable" tooltip="margin-right"></span></p>
-      <p class="margin bottom"><span data-box="margin" class="editable" tooltip="margin-bottom"></span></p>
-      <p class="margin left"><span data-box="margin" class="editable" tooltip="margin-left"></span></p>
+      <p class="margin top"><span data-box="margin" class="editable" title="margin-top"></span></p>
+      <p class="margin right"><span data-box="margin" class="editable" title="margin-right"></span></p>
+      <p class="margin bottom"><span data-box="margin" class="editable" title="margin-bottom"></span></p>
+      <p class="margin left"><span data-box="margin" class="editable" title="margin-left"></span></p>
 
-      <p class="padding top"><span data-box="padding" class="editable" tooltip="padding-top"></span></p>
-      <p class="padding right"><span data-box="padding" class="editable" tooltip="padding-right"></span></p>
-      <p class="padding bottom"><span data-box="padding" class="editable" tooltip="padding-bottom"></span></p>
-      <p class="padding left"><span data-box="padding" class="editable" tooltip="padding-left"></span></p>
+      <p class="padding top"><span data-box="padding" class="editable" title="padding-top"></span></p>
+      <p class="padding right"><span data-box="padding" class="editable" title="padding-right"></span></p>
+      <p class="padding bottom"><span data-box="padding" class="editable" title="padding-bottom"></span></p>
+      <p class="padding left"><span data-box="padding" class="editable" title="padding-left"></span></p>
 
-      <p class="size"><span data-box="content" tooltip="&content.tooltip;"></span></p>
-
-      <span class="tooltip"></span>
+      <p class="size"><span data-box="content" title="&content.tooltip;"></span></p>
 
     </div>
 
     <div style="display: none">
       <p id="dummy"></p>
     </div>
   </body>
 </html>
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -830,32 +830,43 @@ OutputPanel.prototype._init = function(d
   this._update = this._update.bind(this);
 
   // Wire up the element from the iframe, and resolve the promise
   let deferred = promise.defer();
   let onload = () => {
     this._frame.removeEventListener("load", onload, true);
 
     this.document = this._frame.contentDocument;
+    this._copyTheme();
 
     this._div = this.document.getElementById("gcli-output-root");
     this._div.classList.add('gcli-row-out');
     this._div.setAttribute('aria-live', 'assertive');
 
     let styles = this._toolbar.ownerDocument.defaultView
                     .getComputedStyle(this._toolbar);
     this._div.setAttribute("dir", styles.direction);
 
     deferred.resolve(this);
   };
   this._frame.addEventListener("load", onload, true);
 
   return deferred.promise;
 }
 
+/* Copy the current devtools theme attribute into the iframe,
+   so it can be styled correctly. */
+OutputPanel.prototype._copyTheme = function() {
+  if (this.document) {
+    let theme =
+      this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+    this.document.documentElement.setAttribute("devtoolstheme", theme);
+  }
+};
+
 /**
  * Prevent the popup from hiding if it is not permitted via this.canHide.
  */
 OutputPanel.prototype._onpopuphiding = function(ev) {
   // TODO: When we switch back from tooltip to panel we can remove this hack:
   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   if (isLinux && !this.canHide) {
     ev.preventDefault();
@@ -870,16 +881,17 @@ OutputPanel.prototype.show = function() 
     this.canHide = false;
   }
 
   // We need to reset the iframe size in order for future size calculations to
   // be correct
   this._frame.style.minHeight = this._frame.style.maxHeight = 0;
   this._frame.style.minWidth = 0;
 
+  this._copyTheme();
   this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null);
   this._resize();
 
   this._input.focus();
 };
 
 /**
  * Internal helper to set the height of the output panel to fit the available
@@ -1085,16 +1097,17 @@ TooltipPanel.create = function(devtoolba
 
 /**
  * @private See TooltipPanel.create
  */
 TooltipPanel.prototype._init = function(devtoolbar) {
   let deferred = promise.defer();
 
   let chromeDocument = devtoolbar._doc;
+  this._devtoolbar = devtoolbar;
   this._input = devtoolbar._doc.querySelector(".gclitoolbar-input-node");
   this._toolbar = devtoolbar._doc.querySelector("#developer-toolbar");
   this._dimensions = { start: 0, end: 0 };
 
   /*
   <tooltip|panel id="gcli-tooltip"
          type="arrow"
          noautofocus="true"
@@ -1141,29 +1154,40 @@ TooltipPanel.prototype._init = function(
 
   /**
    * Wire up the element from the iframe, and resolve the promise.
    */
   let onload = () => {
     this._frame.removeEventListener("load", onload, true);
 
     this.document = this._frame.contentDocument;
+    this._copyTheme();
     this.hintElement = this.document.getElementById("gcli-tooltip-root");
     this._connector = this.document.getElementById("gcli-tooltip-connector");
 
     let styles = this._toolbar.ownerDocument.defaultView
                     .getComputedStyle(this._toolbar);
     this.hintElement.setAttribute("dir", styles.direction);
 
     deferred.resolve(this);
   };
   this._frame.addEventListener("load", onload, true);
 
   return deferred.promise;
-}
+};
+
+/* Copy the current devtools theme attribute into the iframe,
+   so it can be styled correctly. */
+TooltipPanel.prototype._copyTheme = function() {
+  if (this.document) {
+    let theme =
+      this._devtoolbar._doc.documentElement.getAttribute("devtoolstheme");
+    this.document.documentElement.setAttribute("devtoolstheme", theme);
+  }
+};
 
 /**
  * Prevent the popup from hiding if it is not permitted via this.canHide.
  */
 TooltipPanel.prototype._onpopuphiding = function(ev) {
   // TODO: When we switch back from tooltip to panel we can remove this hack:
   // https://bugzilla.mozilla.org/show_bug.cgi?id=780102
   if (isLinux && !this.canHide) {
@@ -1186,16 +1210,17 @@ TooltipPanel.prototype.show = function(d
   this._panel.ownerDocument.defaultView.setTimeout(() => {
     this._resize();
   }, 0);
 
   if (isLinux) {
     this.canHide = false;
   }
 
+  this._copyTheme();
   this._resize();
   this._panel.openPopup(this._input, "before_start", dimensions.start * 10, 0,
                         false, false, null);
   this._input.focus();
 };
 
 /**
  * One option is to spend lots of time taking an average width of characters
--- a/browser/devtools/shared/test/browser_toolbar_tooltip.js
+++ b/browser/devtools/shared/test/browser_toolbar_tooltip.js
@@ -6,35 +6,36 @@
 ///////////////////
 //
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Protocol error (unknownError): Error: Got an invalid root window in DocumentWalker");
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
 
-function test() {
-  addTab(TEST_URI, function() {
-    Task.spawn(runTest).catch(err => {
-      ok(false, ex);
-      console.error(ex);
-    }).then(finish);
-  });
-}
+registerCleanupFunction(() => {
+  // Set preferences back to their original values
+  Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+});
 
-function* runTest() {
+add_task(function* showToolbar() {
+  yield promiseTab(TEST_URI);
+
   info("Starting browser_toolbar_tooltip.js");
 
   ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
 
   let showPromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.SHOW);
   document.getElementById("Tools:DevToolbar").doCommand();
   yield showPromise;
+});
 
+add_task(function* testDimensions() {
   let tooltipPanel = DeveloperToolbar.tooltipPanel;
 
   DeveloperToolbar.display.focusManager.helpRequest();
   yield DeveloperToolbar.display.inputter.setInput('help help');
 
   DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length });
   is(tooltipPanel._dimensions.start, 'help '.length,
           'search param start, when cursor at end');
@@ -49,17 +50,36 @@ function* runTest() {
   is(tooltipPanel._dimensions.start, 'help '.length,
           'search param start, when cursor at penultimate position');
   ok(getLeftMargin() > 30, 'tooltip offset, when cursor at penultimate position')
 
   DeveloperToolbar.display.inputter.setCursor({ start: 0 });
   is(tooltipPanel._dimensions.start, 0,
           'search param start, when cursor at start');
   ok(getLeftMargin() > 9, 'tooltip offset, when cursor at start')
-}
+});
+
+add_task(function* testThemes() {
+  let tooltipPanel = DeveloperToolbar.tooltipPanel;
+  ok(tooltipPanel.document, "Tooltip panel is initialized");
+
+  Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+
+  yield DeveloperToolbar.display.inputter.setInput("");
+  yield DeveloperToolbar.display.inputter.setInput("help help");
+  is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+     "dark", "Tooltip panel has correct theme");
+
+  Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+
+  yield DeveloperToolbar.display.inputter.setInput("");
+  yield DeveloperToolbar.display.inputter.setInput("help help");
+  is(tooltipPanel.document.documentElement.getAttribute("devtoolstheme"),
+     "light", "Tooltip panel has correct theme");
+});
 
 function getLeftMargin() {
   let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
   return parseInt(style.slice(0, -2), 10);
 }
 
 function observeOnce(topic, ownsWeak=false) {
   return new Promise(function(resolve, reject) {
--- a/browser/devtools/shared/widgets/CubicBezierWidget.js
+++ b/browser/devtools/shared/widgets/CubicBezierWidget.js
@@ -578,30 +578,26 @@ CubicBezierPresetWidget.prototype = {
 
   _createPreset: function(categoryLabel, presetLabel) {
     let doc = this.parent.ownerDocument;
 
     let preset = doc.createElement("div");
     preset.classList.add("preset");
     preset.id = presetLabel;
     preset.coordinates = PRESETS[categoryLabel][presetLabel];
-
     // Create preset preview
     let curve = doc.createElement("canvas");
     let bezier = new CubicBezier(preset.coordinates);
-
-    curve.setAttribute("height", 55);
-    curve.setAttribute("width", 55);
-
+    curve.setAttribute("height", 50);
+    curve.setAttribute("width", 50);
     preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
     preset.bezierCanvas.plot({
       drawHandles: false,
       bezierThickness: 0.025
     });
-
     preset.appendChild(curve);
 
     // Create preset label
     let presetLabelElem = doc.createElement("p");
     let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel);
     presetLabelElem.textContent = presetDisplayLabel;
     preset.appendChild(presetLabelElem);
 
--- a/browser/devtools/shared/widgets/cubic-bezier.css
+++ b/browser/devtools/shared/widgets/cubic-bezier.css
@@ -131,38 +131,37 @@ canvas#curve {
   100% {
     left: -7px;
   }
 }
 
 /* Preset Widget */
 
 .preset-pane {
-  width:50%;
+  width: 50%;
   height: 100%;
   border-right: 1px solid var(--theme-splitter-color);
+  padding-right: 4px; /* Visual balance for the panel-arrowcontent border on the left */
 }
 
 #preset-categories {
   display: flex;
-  width: 94%;
+  width: 95%;
   border: 1px solid var(--theme-splitter-color);
   border-radius: 2px;
   background-color: var(--theme-toolbar-background);
-  margin-left: 4px;
-  margin-top: 3px;
+  margin: 3px auto 0 auto;
 }
 
 #preset-categories .category:last-child {
   border-right: none;
 }
 
 .category {
-  flex: 1 1 auto;
-  padding: 5px;
+  padding: 5px 0px;
   width: 33.33%;
   text-align: center;
   text-transform: capitalize;
   border-right: 1px solid var(--theme-splitter-color);
   cursor: default;
   color: var(--theme-body-color);
 }
 
@@ -178,56 +177,54 @@ canvas#curve {
 .active-category:hover {
   background-color: var(--theme-selection-background);
 }
 
 #preset-container {
   padding: 0px;
   width: 100%;
   height: 331px;
-  overflow-y: scroll;
+  overflow-y: auto;
 }
 
 .preset-list {
   display: none;
+  padding-top: 6px;
 }
 
 .active-preset-list {
   display: flex;
   flex-wrap: wrap;
-  justify-content: left;
-  padding-left: 4px;
-  padding-top: 3px;
+  justify-content: flex-start;
 }
 
 .preset {
   cursor: pointer;
-  width: 55px;
-  margin: 5px 11px 0px 0px;
-  text-transform: capitalize;
+  width: 33.33%;
+  margin: 5px 0px;
   text-align: center;
 }
 
 .preset canvas {
   display: block;
   border: 1px solid #ccc;
   border-radius: 3px;
   background-color: var(--theme-body-background);
+  margin: 0 auto;
 }
 
 .theme-dark .preset canvas {
   border-color: #444e58;
 }
 
 .preset p {
-  text-align: center;
   font-size: 0.9em;
-  line-height: 0px;
-  margin: 2px 0px 0px 0p;
+  margin: 2px auto 0px auto;
   color: var(--theme-body-color-alt);
+  text-transform: capitalize;
 }
 
 .active-preset p, .active-preset:hover p {
   color: var(--theme-body-color);
 }
 
 .preset:hover canvas {
   border-color: var(--theme-selection-background);
--- a/browser/devtools/sourceeditor/autocomplete.js
+++ b/browser/devtools/sourceeditor/autocomplete.js
@@ -107,19 +107,17 @@ function initializeAutoCompletion(ctx, o
   function insertSelectedPopupItem() {
     let autocompleteState = autocompleteMap.get(ed);
     if (!popup || !popup.isOpen || !autocompleteState) {
       return;
     }
 
     if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
       autocompleteMap.get(ed).insertingSuggestion = true;
-      let {label, preLabel, text} = popup.selectedItem;
-      let cur = ed.getCursor();
-      ed.replaceText(text.slice(preLabel.length), cur, cur);
+      insertPopupItem(ed, popup.selectedItem);
     }
 
     popup.hidePopup();
     ed.emit("popup-hidden"); // This event is used in tests.
     return true;
   }
 
   let popup = new AutocompletePopup(win.parent.document, {
@@ -222,16 +220,38 @@ function autoComplete({ ed, cm }) {
     popup.openPopup(cursorElement, -1 * left, 0);
     private.suggestionInsertedOnce = false;
     // This event is used in tests.
     ed.emit("after-suggest");
   }).then(null, Cu.reportError);
 }
 
 /**
+ * Inserts a popup item into the current cursor location
+ * in the editor.
+ */
+function insertPopupItem(ed, popupItem) {
+  let {label, preLabel, text} = popupItem;
+  let cur = ed.getCursor();
+  let textBeforeCursor = ed.getText(cur.line).substring(0, cur.ch);
+  let backwardsTextBeforeCursor = textBeforeCursor.split("").reverse().join("");
+  let backwardsPreLabel = preLabel.split("").reverse().join("");
+
+  // If there is additional text in the preLabel vs the line, then
+  // just insert the entire autocomplete text.  An example:
+  // if you type 'a' and select '#about' from the autocomplete menu,
+  // then the final text needs to the end up as '#about'.
+  if (backwardsPreLabel.indexOf(backwardsTextBeforeCursor) === 0) {
+    ed.replaceText(text, {line: cur.line, ch: 0}, cur);
+  } else {
+    ed.replaceText(text.slice(preLabel.length), cur, cur);
+  }
+}
+
+/**
  * Cycles through provided suggestions by the popup in a top to bottom manner
  * when `reverse` is not true. Opposite otherwise.
  */
 function cycleSuggestions(ed, reverse) {
   let private = autocompleteMap.get(ed);
   let { popup, completer } = private;
   let cur = ed.getCursor();
   private.insertingSuggestion = true;
@@ -245,17 +265,17 @@ function cycleSuggestions(ed, reverse) {
       firstItem = popup.getItemAtIndex(0);
       if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
         firstItem = popup.getItemAtIndex(1);
         popup.selectNextItem();
       }
     }
     if (popup.itemCount == 1)
       popup.hidePopup();
-    ed.replaceText(firstItem.text.slice(firstItem.preLabel.length), cur, cur);
+    insertPopupItem(ed, firstItem);
   } else {
     let fromCur = {
       line: cur.line,
       ch  : cur.ch - popup.selectedItem.text.length
     };
     if (reverse)
       popup.selectPreviousItem();
     else
--- a/browser/devtools/sourceeditor/css-autocompleter.js
+++ b/browser/devtools/sourceeditor/css-autocompleter.js
@@ -752,49 +752,64 @@ CSSCompleter.prototype = {
   */
   prepareSelectorResults: function(result) {
     if (this._currentQuery != result.query)
       return [];
 
     result = result.suggestions;
     let query = this.selector;
     let completion = [];
-    for (let value of result) {
+    for (let [value, count, state] of result) {
       switch(this.selectorState) {
         case SELECTOR_STATES.id:
         case SELECTOR_STATES.class:
         case SELECTOR_STATES.pseudo:
           if (/^[.:#]$/.test(this.completing)) {
-            value[0] = query.slice(0, query.length - this.completing.length) +
-                       value[0];
+            value = query.slice(0, query.length - this.completing.length) +
+                       value;
           } else {
-            value[0] = query.slice(0, query.length - this.completing.length - 1) +
-                       value[0];
+            value = query.slice(0, query.length - this.completing.length - 1) +
+                       value;
           }
           break;
 
         case SELECTOR_STATES.tag:
-          value[0] = query.slice(0, query.length - this.completing.length) +
-                     value[0];
+          value = query.slice(0, query.length - this.completing.length) +
+                     value;
           break;
 
         case SELECTOR_STATES.null:
-          value[0] = query + value[0];
+          value = query + value;
           break;
 
         default:
-         value[0] = query.slice(0, query.length - this.completing.length) +
-                    value[0];
+         value = query.slice(0, query.length - this.completing.length) +
+                    value;
       }
-      completion.push({
-        label: value[0],
+
+      let item = {
+        label: value,
         preLabel: query,
-        text: value[0],
-        score: value[1]
-      });
+        text: value,
+        score: count
+      };
+
+      // In case the query's state is tag and the item's state is id or class
+      // adjust the preLabel
+      if (this.selectorState === SELECTOR_STATES.tag &&
+          state === SELECTOR_STATES.class) {
+        item.preLabel = "." + item.preLabel;
+      }
+      if (this.selectorState === SELECTOR_STATES.tag &&
+          state === SELECTOR_STATES.id) {
+        item.preLabel = "#" + item.preLabel;
+      }
+
+      completion.push(item);
+
       if (completion.length > this.maxEntries - 1)
         break;
     }
     return completion;
   },
 
   /**
    * Returns CSS property name suggestions based on the input.
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -458,18 +458,18 @@ Editor.prototype = {
     }
 
     cm.setOption("indentUnit", indentUnit);
     cm.setOption("indentWithTabs", indentWithTabs);
   },
 
   /**
    * Replaces contents of a text area within the from/to {line, ch}
-   * range. If neither from nor to arguments are provided works
-   * exactly like setText. If only from object is provided, inserts
+   * range. If neither `from` nor `to` arguments are provided works
+   * exactly like setText. If only `from` object is provided, inserts
    * text at that point, *overwriting* as many characters as needed.
    */
   replaceText: function (value, from, to) {
     let cm = editors.get(this);
 
     if (!from) {
       this.setText(value);
       return;
--- a/browser/devtools/sourceeditor/test/browser_editor_autocomplete_events.js
+++ b/browser/devtools/sourceeditor/test/browser_editor_autocomplete_events.js
@@ -1,17 +1,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {InspectorFront} = require("devtools/server/actors/inspector");
 const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
-const TEST_URI = "data:text/html;charset=UTF-8,<html><body><b1></b1><b2></b2><body></html>";
+const TEST_URI = "data:text/html;charset=UTF-8,<html><body><bar></bar><div id='baz'></div><body></html>";
 
 add_task(function*() {
   yield promiseTab(TEST_URI);
   yield runTests();
 });
 
 function* runTests() {
   let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
@@ -20,16 +20,18 @@ function* runTests() {
   let walker = yield inspector.getWalker();
   let {ed, win, edWin} = yield setup(null, {
     autocomplete: true,
     mode: Editor.modes.css,
     autocompleteOpts: {walker: walker}
   });
   yield testMouse(ed, edWin);
   yield testKeyboard(ed, edWin);
+  yield testKeyboardCycle(ed, edWin);
+  yield testKeyboardCycleForPrefixedString(ed, edWin);
   teardown(ed, win);
 }
 
 function* testKeyboard(ed, win) {
   ed.focus();
   ed.setText("b");
   ed.setCursor({line: 1, ch: 1});
 
@@ -37,26 +39,66 @@ function* testKeyboard(ed, win) {
 
   let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
   EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
 
   info ("Waiting for popup to be opened");
   yield popupOpened;
 
   EventUtils.synthesizeKey("VK_RETURN", { }, win);
-  is (ed.getText(), "b1", "Editor text has been updated");
+  is (ed.getText(), "bar", "Editor text has been updated");
+}
+
+function* testKeyboardCycle(ed, win) {
+  ed.focus();
+  ed.setText("b");
+  ed.setCursor({line: 1, ch: 1});
+
+  let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+  let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+  EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+  info ("Waiting for popup to be opened");
+  yield popupOpened;
+
+  EventUtils.synthesizeKey("VK_DOWN", { }, win);
+  is (ed.getText(), "bar", "Editor text has been updated");
+
+  EventUtils.synthesizeKey("VK_DOWN", { }, win);
+  is (ed.getText(), "body", "Editor text has been updated");
+
+  EventUtils.synthesizeKey("VK_DOWN", { }, win);
+  is (ed.getText(), "#baz", "Editor text has been updated");
+}
+
+function* testKeyboardCycleForPrefixedString(ed, win) {
+  ed.focus();
+  ed.setText("#b");
+  ed.setCursor({line: 1, ch: 2});
+
+  let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+  let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+  EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+  info ("Waiting for popup to be opened");
+  yield popupOpened;
+
+  EventUtils.synthesizeKey("VK_DOWN", { }, win);
+  is (ed.getText(), "#baz", "Editor text has been updated");
 }
 
 function* testMouse(ed, win) {
   ed.focus();
   ed.setText("b");
   ed.setCursor({line: 1, ch: 1});
 
   let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
 
   let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
   EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
 
   info ("Waiting for popup to be opened");
   yield popupOpened;
-  ed.getAutocompletionPopup()._list.firstChild.click();
-  is (ed.getText(), "b1", "Editor text has been updated");
+  ed.getAutocompletionPopup()._list.children[2].click();
+  is (ed.getText(), "#baz", "Editor text has been updated");
 }
--- a/browser/devtools/sourceeditor/test/css_autocompletion_tests.json
+++ b/browser/devtools/sourceeditor/test/css_autocompletion_tests.json
@@ -16,21 +16,21 @@
              '-moz-animation-play-state', '-moz-animation-timing-function',
              '-moz-appearance']],
   [[12, 20], ['none', 'number-input']],
   [[12, 22], ['none']],
   [[17, 22], ['hsl', 'hsla']],
   [[21,  9], ["-moz-calc", "auto", "calc", "inherit", "initial","unset"]],
   [[22,  5], ['color', 'color-interpolation', 'color-interpolation-filters']],
   [[25, 26], ['.devtools-toolbarbutton > tab',
-              '.devtools-toolbarbutton > .toolbarbutton-menubutton-button',
-              '.devtools-toolbarbutton > hbox']],
+              '.devtools-toolbarbutton > hbox',
+              '.devtools-toolbarbutton > .toolbarbutton-menubutton-button']],
   [[25, 31], ['.devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button']],
   [[29, 20], ['.devtools-menulist:after', '.devtools-menulist:active']],
   [[30, 10], ['#devtools-anotherone', '#devtools-itjustgoeson', '#devtools-menu',
               '#devtools-okstopitnow', '#devtools-toolbarbutton', '#devtools-yetagain']],
   [[39, 39], ['.devtools-toolbarbutton:not([label]) > tab']],
   [[43, 51], ['.devtools-toolbarbutton:not([checked=true]):hover:after',
               '.devtools-toolbarbutton:not([checked=true]):hover:active']],
   [[58, 36], ['!important;']],
-  [[73, 42], [':last-child', ':lang(', ':last-of-type', ':link']],
+  [[73, 42], [':lang(', ':last-of-type', ':link', ':last-child']],
   [[77, 25], ['.visible']],
 ]
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -418,16 +418,26 @@ These should match what Safari and other
 <!ENTITY searchFocus.commandkey2      "e">
 <!ENTITY searchFocusUnix.commandkey   "j">
 
 <!-- LOCALIZATION NOTE (searchFor.label, searchWith.label):
      These two strings are used to build the header above the list of one-click
      search providers:  "Search for <used typed keywords> with:" -->
 <!ENTITY searchFor.label              "Search for ">
 <!ENTITY searchWith.label             " with:">
+
+<!-- LOCALIZATION NOTE (search.label, searchAfter.label):
+     This string is used to build the header above the list of one-click search
+     providers when a one off engine has been selected.  The searchAfter text is
+     intentionally left empty for en-US and can be used by other localizations to
+     display a string after the search engine name.  This string will be displayed
+     as:  "Search <selected engine name><searchAfter.label text>" -->
+<!ENTITY search.label                 "Search ">
+<!ENTITY searchAfter.label            "">
+
 <!-- LOCALIZATION NOTE (searchWithHeader.label):
      The wording of this string should be as close as possible to
      searchFor.label and searchWith.label. This string will be used instead of
      them when the user has not typed any keyword. -->
 <!ENTITY searchWithHeader.label       "Search with:">
 <!-- LOCALIZATION NOTE (changeSearchSettings.button):
      This string won't wrap, so if the translated string is longer,
      consider translating it as if it said only "Search Settings". -->
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -154,16 +154,21 @@ let ReaderParent = {
       let enterText = gStringBundle.GetStringFromName("readerView.enter");
       button.setAttribute("tooltiptext", enterText);
       command.setAttribute("label", enterText);
       command.setAttribute("hidden", !browser.isArticle);
       command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
     }
   },
 
+  forceShowReaderIcon: function(browser) {
+    browser.isArticle = true;
+    this.updateReaderButton(browser);
+  },
+
   buttonClick: function(event) {
     if (event.button != 0) {
       return;
     }
     this.toggleReaderMode(event);
   },
 
   toggleReaderMode: function(event) {
--- a/browser/themes/shared/devtools/commandline.css
+++ b/browser/themes/shared/devtools/commandline.css
@@ -1,24 +1,42 @@
 /* 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/. */
 
+
+/* NOTE: THESE NEED TO STAY IN SYNC WITH LIGHT-THEME.CSS AND DARK-THEME.CSS.
+   We are copy/pasting variables from light-theme and dark-theme,
+   since they aren't loaded in this context (within commandlineoutput.xhtml
+   and commandlinetooltip.xhtml). */
+:root[devtoolstheme="light"] {
+  --gcli-background-color: #ebeced; /* --theme-tab-toolbar-background */
+  --gcli-input-focused-background: #f7f7f7; /* --theme-sidebar-background */
+  --gcli-input-color: #18191a; /* --theme-body-color */
+  --gcli-border-color: #aaaaaa; /* --theme-splitter-color */
+}
+
+:root[devtoolstheme="dark"] {
+  --gcli-background-color: #343c45; /* --theme-toolbar-background */
+  --gcli-input-focused-background: #252c33; /* --theme-tab-toolbar-background */
+  --gcli-input-color: #b6babf; /* --theme-body-color-alt */
+  --gcli-border-color: black; /* --theme-splitter-color */
+}
+
 .gcli-body {
   margin: 0;
   font: message-box;
-  color: hsl(210,30%,85%);
+  color: var(--gcli-input-color);
 }
 
 #gcli-output-root,
 #gcli-tooltip-root {
-  border: 1px solid hsl(206,37%,4%);
-  box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
-  background-image: linear-gradient(hsla(209,18%,18%,0.9), hsl(209,23%,18%));
+  border: 1px solid var(--gcli-border-color);
   border-radius: 3px;
+  background-color: var(--gcli-background-color);
 }
 
 #gcli-output-root {
   padding: 5px 10px;
   border-bottom-left-radius: 0;
   border-bottom-right-radius: 0;
   border-bottom: 0;
 }
@@ -27,32 +45,32 @@
   padding: 5px 0px;
 }
 
 #gcli-tooltip-connector {
   margin-top: -1px;
   margin-left: 8px;
   width: 20px;
   height: 10px;
-  border-left: 1px solid hsl(206,37%,4%);
-  border-right: 1px solid hsl(206,37%,4%);
-  background-color: hsl(209,23%,18%);
+  border-left: 1px solid var(--gcli-border-color);
+  border-right: 1px solid var(--gcli-border-color);
+  background-color: var(--gcli-background-color);
 }
 
 .gcli-tt-description,
 .gcli-tt-error {
   padding: 0 10px;
 }
 
 .gcli-row-out {
   padding: 0 5px;
   line-height: 1.2em;
   border-top: none;
   border-bottom: none;
-  color: hsl(210,30%,85%);
+  color: var(--gcli-input-color);
 }
 
 .gcli-row-out p,
 .gcli-row-out h1,
 .gcli-row-out h2,
 .gcli-row-out h3 {
   margin: 5px 0;
 }
@@ -60,17 +78,17 @@
 .gcli-row-out h1,
 .gcli-row-out h2,
 .gcli-row-out h3,
 .gcli-row-out h4,
 .gcli-row-out h5,
 .gcli-row-out th,
 .gcli-row-out strong,
 .gcli-row-out pre {
-  color: hsl(210,30%,95%);
+  color: var(--gcli-input-color);
 }
 
 .gcli-row-out pre {
   font-size: 80%;
 }
 
 .gcli-row-out td {
   white-space: nowrap;
@@ -78,23 +96,23 @@
 
 .gcli-out-shortcut,
 .gcli-help-synopsis {
   padding: 0 3px;
   margin: 0 4px;
   font-weight: normal;
   font-size: 90%;
   border-radius: 3px;
-  background-color: hsl(209,23%,18%);
-  border: 1px solid hsl(206,37%,4%);
+  background-color: var(--gcli-background-color);
+  border: 1px solid var(--gcli-border-color);
 }
 
 .gcli-out-shortcut:before,
 .gcli-help-synopsis:before {
-  color: hsl(210,30%,85%);
+  color: var(--gcli-input-color);
   -moz-padding-end: 2px;
 }
 
 .gcli-help-arrow {
   color: #666;
 }
 
 .gcli-help-description {
@@ -112,22 +130,22 @@
 }
 
 .gcli-menu-name {
   -moz-padding-start: 8px;
 }
 
 .gcli-menu-desc {
   -moz-padding-end: 8px;
-  color: hsl(210,30%,75%);
+  color: var(--gcli-input-color);
 }
 
 .gcli-menu-name:hover,
 .gcli-menu-desc:hover {
-  background-color: hsla(0,0%,0%,.3);
+  background-color: var(--gcli-input-focused-background);
 }
 
 .gcli-menu-highlight,
 .gcli-menu-highlight:hover {
   background-color: hsla(0,100%,100%,.1);
 }
 
 .gcli-menu-typed {
--- a/browser/themes/shared/devtools/commandline.inc.css
+++ b/browser/themes/shared/devtools/commandline.inc.css
@@ -1,38 +1,65 @@
 %if 0
 /* 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/. */
 %endif
 
 /* Developer toolbar */
 
+/* NOTE: THESE NEED TO STAY IN SYNC WITH LIGHT-THEME.CSS AND DARK-THEME.CSS.
+   We are copy/pasting variables from light-theme and dark-theme,
+   since they aren't loaded in this context (within browser.css). */
+:root[devtoolstheme="light"] #developer-toolbar {
+  --gcli-background-color: #ebeced; /* --theme-tab-toolbar-background */
+  --gcli-input-background: #f0f1f2; /* --theme-toolbar-background */
+  --gcli-input-focused-background: #f7f7f7; /* --theme-sidebar-background */
+  --gcli-input-color: #18191a; /* --theme-body-color */
+  --gcli-border-color: #aaaaaa; /* --theme-splitter-color */
+  --selection-background: #4c9ed9; /* --theme-selection-background */
+  --selection-color: #f5f7fa; /* --theme-selection-color */
+}
+
+:root[devtoolstheme="dark"] #developer-toolbar {
+  --gcli-background-color: #343c45; /* --theme-toolbar-background */
+  --gcli-input-background: rgba(37, 44, 51, .6); /* --theme-tab-toolbar-background */
+  --gcli-input-focused-background: #252c33; /* --theme-tab-toolbar-background */
+  --gcli-input-color: #b6babf; /* --theme-body-color-alt */
+  --gcli-border-color: black; /* --theme-splitter-color */
+  --selection-background: #1d4f73; /* --theme-selection-background */
+  --selection-color: #f5f7fa; /* --theme-selection-color */
+}
+
 #developer-toolbar {
   -moz-appearance: none;
   padding: 0;
   min-height: 32px;
-  background-color: #343C45; /* Toolbars */
-  border-top: 1px solid #060a0d;
-  box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset, 0 -1px 0 hsla(206,37%,4%,.1) inset;
+  background-color: var(--gcli-background-color);
+  border-top: 1px solid var(--gcli-border-color);
 }
 
 #developer-toolbar > toolbarbutton {
   -moz-appearance: none;
   border: none;
   background: transparent;
   margin: 0;
   padding: 0 10px;
   width: 32px;
 }
 
 .developer-toolbar-button > image {
   margin: auto 10px;
 }
 
+:root[devtoolstheme="light"] #developer-toolbar > toolbarbutton:not([checked=true]) > image,
+:root[devtoolstheme="light"] .gclitoolbar-input-node:not([focused=true])::before  {
+  filter: url("chrome://browser/skin/devtools/filters.svg#invert");
+}
+
 .developer-toolbar-button > .toolbarbutton-icon,
 #developer-toolbar-closebutton > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
 #developer-toolbar-toolbox-button {
   list-style-image: url("chrome://browser/skin/devtools/toggle-tools.png");
@@ -130,34 +157,35 @@ html|*#gcli-output-frame {
   -moz-box-align: center;
   padding-top: 0;
   padding-bottom: 0;
   padding-right: 8px;
   text-shadow: none;
   box-shadow: none;
   border-width: 0;
   background-color: transparent;
+  border-radius: 0;
 }
 
 .gclitoolbar-input-node {
   -moz-appearance: none;
-  color: hsl(210,30%,85%);
-  background-color: #242b33;
+  color: var(--gcli-input-color);
+  background-color: var(--gcli-input-background);
   background-repeat: no-repeat;
   background-position: 4px center;
-  box-shadow: 0 1px 1px hsla(206,37%,4%,.2) inset,
-              1px 0 0 hsla(206,37%,4%,.2) inset,
-              -1px 0 0 hsla(206,37%,4%,.2) inset;
+  box-shadow: 1px 0 0 var(--gcli-border-color) inset,
+              -1px 0 0 var(--gcli-border-color) inset;
+
   line-height: 32px;
   outline-style: none;
   padding: 0;
 }
 
 .gclitoolbar-input-node[focused="true"] {
-  background-color: #232e38;
+  background-color: var(--gcli-input-focused-background);
 }
 
 .gclitoolbar-input-node::before {
   content: "";
   display: inline-block;
   -moz-box-ordinal-group: 0;
   width: 16px;
   height: 16px;
@@ -173,18 +201,18 @@ html|*#gcli-output-frame {
 
 @media (min-resolution: 2dppx) {
   .gclitoolbar-input-node::before {
     background-image: url("chrome://browser/skin/devtools/commandline-icon@2x.png");
   }
 }
 
 .gclitoolbar-input-node > .textbox-input-box > html|*.textbox-input::-moz-selection {
-  background-color: hsl(210,30%,85%);
-  color: hsl(210,24%,16%);
+  background-color: var(--selection-background);
+  color: var(--selection-color);
   text-shadow: none;
 }
 
 .gclitoolbar-complete-node {
   padding-left: 21px;
   background-color: transparent;
   color: transparent;
   z-index: 100;
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -1,18 +1,18 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 /* Colors are taken from:
- * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
+ * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors.
+ * Changes should be kept in sync with commandline.css and commandline.inc.css.
  */
 :root {
-
   --theme-body-background: #14171a;
   --theme-sidebar-background: #181d20;
   --theme-contrast-background: #b28025;
 
   --theme-tab-toolbar-background: #252c33;
   --theme-toolbar-background: #343c45;
   --theme-selection-background: #1d4f73;
   --theme-selection-background-semitransparent: rgba(29, 79, 115, .5);
--- a/browser/themes/shared/devtools/layoutview.css
+++ b/browser/themes/shared/devtools/layoutview.css
@@ -42,17 +42,17 @@
 
 #margins {
   background-color: #edff64;
   /* This opacity applies to all of the regions, since they are nested. */
   opacity: .8;
 }
 
 .editable {
-  border-bottom: 1px dashed transparent;
+  border: 1px dashed transparent;
 }
 
 .editable:hover {
   border-bottom-color: hsl(0,0%,50%);
 }
 
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -1,15 +1,16 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 /* Colors are taken from:
- * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
+ * https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors.
+ * Changes should be kept in sync with commandline.css and commandline.inc.css.
  */
 :root {
   --theme-body-background: #fcfcfc;
   --theme-sidebar-background: #f7f7f7;
   --theme-contrast-background: #e6b064;
 
   --theme-tab-toolbar-background: #ebeced;
   --theme-toolbar-background: #f0f1f2;
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -312,37 +312,8 @@
   @media not all and (-moz-os-version: windows-win7) {
     /* Introducing an additional hover state for the Bookmark button */
     #nav-bar .toolbarbutton-1[buttonover] > .toolbarbutton-menubutton-button:hover > .toolbarbutton-icon {
       background-color: hsla(210,4%,10%,.08);
       border-color: hsla(210,4%,10%,.1);
     }
   }
 }
-
-/* ::::: fullscreen window controls ::::: */
-
-#window-controls {
-  -moz-box-align: start;
-}
-
-#minimize-button,
-#restore-button,
-#close-button {
-  -moz-appearance: none;
-  border-style: none;
-  margin: 0;
-}
-#close-button {
-  -moz-image-region: rect(0, 49px, 16px, 32px);
-}
-#close-button:hover {
-  -moz-image-region: rect(16px, 49px, 32px, 32px);
-}
-#close-button:hover:active {
-  -moz-image-region: rect(32px, 49px, 48px, 32px);
-}
-
-#minimize-button:-moz-locale-dir(rtl),
-#restore-button:-moz-locale-dir(rtl),
-#close-button:-moz-locale-dir(rtl) {
-  transform: scaleX(-1);
-}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1123,16 +1123,45 @@ toolbarbutton[constrain-size="true"][cui
 }
 #close-button:hover {
   -moz-image-region: rect(16px, 48px, 32px, 32px);
 }
 #close-button:hover:active {
   -moz-image-region: rect(32px, 48px, 48px, 32px);
 }
 
+@media not all and (-moz-os-version: windows-xp) {
+  #window-controls {
+    -moz-box-align: start;
+  }
+
+  #minimize-button,
+  #restore-button,
+  #close-button {
+    -moz-appearance: none;
+    border-style: none;
+    margin: 0;
+  }
+  #close-button {
+    -moz-image-region: rect(0, 49px, 16px, 32px);
+  }
+  #close-button:hover {
+    -moz-image-region: rect(16px, 49px, 32px, 32px);
+  }
+  #close-button:hover:active {
+    -moz-image-region: rect(32px, 49px, 48px, 32px);
+  }
+
+  #minimize-button:-moz-locale-dir(rtl),
+  #restore-button:-moz-locale-dir(rtl),
+  #close-button:-moz-locale-dir(rtl) {
+    transform: scaleX(-1);
+  }
+}
+
 /* ::::: Location Bar ::::: */
 
 #urlbar,
 .searchbar-textbox {
   -moz-appearance: none;
   margin: 0 3px;
   padding: 0;
   background-clip: padding-box;
--- a/mobile/android/components/AboutRedirector.js
+++ b/mobile/android/components/AboutRedirector.js
@@ -57,16 +57,17 @@ let modules = {
   },
   downloads: {
     uri: "chrome://browser/content/aboutDownloads.xhtml",
     privileged: true
   },
   reader: {
     uri: "chrome://global/content/reader/aboutReader.html",
     privileged: false,
+    dontLink: true,
     hide: true
   },
   feedback: {
     uri: "chrome://browser/content/aboutFeedback.xhtml",
     privileged: true
   },
   privatebrowsing: {
     uri: "chrome://browser/content/aboutPrivateBrowsing.xhtml",
@@ -104,16 +105,18 @@ AboutRedirector.prototype = {
   },
 
   // nsIAboutModule
   getURIFlags: function(aURI) {
     let flags;
     let moduleInfo = this._getModuleInfo(aURI);
     if (moduleInfo.hide)
       flags = Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+    if (moduleInfo.dontLink)
+      flags = flags | Ci.nsIAboutModule.MAKE_UNLINKABLE;
 
     return flags | Ci.nsIAboutModule.ALLOW_SCRIPT;
   },
 
   newChannel: function(aURI, aLoadInfo) {
     let moduleInfo = this._getModuleInfo(aURI);
 
     var ios = Cc["@mozilla.org/network/io-service;1"].
--- a/netwerk/protocol/about/nsAboutProtocolHandler.cpp
+++ b/netwerk/protocol/about/nsAboutProtocolHandler.cpp
@@ -23,16 +23,24 @@ static NS_DEFINE_CID(kNestedAboutURICID,
 
 static bool IsSafeForUntrustedContent(nsIAboutModule *aModule, nsIURI *aURI) {
   uint32_t flags;
   nsresult rv = aModule->GetURIFlags(aURI, &flags);
   NS_ENSURE_SUCCESS(rv, false);
 
   return (flags & nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT) != 0;
 }
+
+static bool IsSafeToLinkForUntrustedContent(nsIAboutModule *aModule, nsIURI *aURI) {
+  uint32_t flags;
+  nsresult rv = aModule->GetURIFlags(aURI, &flags);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  return (flags & nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT) && !(flags & nsIAboutModule::MAKE_UNLINKABLE);
+}
 ////////////////////////////////////////////////////////////////////////////////
 
 NS_IMPL_ISUPPORTS(nsAboutProtocolHandler, nsIProtocolHandler)
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsIProtocolHandler methods:
 
 NS_IMETHODIMP
@@ -77,17 +85,17 @@ nsAboutProtocolHandler::NewURI(const nsA
     // Unfortunately, people create random about: URIs that don't correspond to
     // about: modules...  Since those URIs will never open a channel, might as
     // well consider them unsafe for better perf, and just in case.
     bool isSafe = false;
     
     nsCOMPtr<nsIAboutModule> aboutMod;
     rv = NS_GetAboutModule(url, getter_AddRefs(aboutMod));
     if (NS_SUCCEEDED(rv)) {
-        isSafe = IsSafeForUntrustedContent(aboutMod, url);
+        isSafe = IsSafeToLinkForUntrustedContent(aboutMod, url);
     }
 
     if (isSafe) {
         // We need to indicate that this baby is safe.  Use an inner URI that
         // no one but the security manager will see.  Make sure to preserve our
         // path, in case someone decides to hardcode checks for particular
         // about: URIs somewhere.
         nsAutoCString spec;
--- a/netwerk/protocol/about/nsIAboutModule.idl
+++ b/netwerk/protocol/about/nsIAboutModule.idl
@@ -20,20 +20,20 @@ interface nsIAboutModule : nsISupports
      * @param aLoadInfo the loadinfo of the new channel
      */
     nsIChannel newChannel(in nsIURI aURI,
                           in nsILoadInfo aLoadInfo);
 
     /**
      * A flag that indicates whether a URI is safe for untrusted
      * content.  If it is, web pages and so forth will be allowed to
-     * link to this about: URI, and the about: protocol handler will
-     * enforce that the principal of channels created for it be based
-     * on their originalURI or URI (depending on the channel flags),
-     * by setting their "owner" to null.
+     * link to this about: URI (unless MAKE_UNLINKABLE is also specified),
+     * and the about: protocol handler will enforce that the principal
+     * of channels created for it be based on their originalURI or URI
+     * (depending on the channel flags), by setting their "owner" to null.
      * Otherwise, only chrome will be able to link to it.
      */
     const unsigned long URI_SAFE_FOR_UNTRUSTED_CONTENT = (1 << 0);
 
     /**
      * A flag that indicates whether script should be enabled for the
      * given about: URI even if it's disabled in general.
      */
@@ -56,16 +56,22 @@ interface nsIAboutModule : nsISupports
     const unsigned long URI_CAN_LOAD_IN_CHILD = (1 << 4);
 
     /**
      * A flag that indicates that this URI must be loaded in a child process
      */
     const unsigned long URI_MUST_LOAD_IN_CHILD = (1 << 5);
 
     /**
+     * A flag that indicates that this URI should be unlinkable despite being
+     * safe for untrusted content.
+     */
+    const unsigned long MAKE_UNLINKABLE = (1 << 6);
+
+    /**
      * A method to get the flags that apply to a given about: URI.  The URI
      * passed in is guaranteed to be one of the URIs that this module
      * registered to deal with.
      */
     unsigned long getURIFlags(in nsIURI aURI);
 
     /**
      * Returns the Indexed DB origin's postfix used for the given about: URI.
--- a/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
@@ -35,46 +35,57 @@
       tr.platform {
         background-color: rgb(255, 255, 200);
       }
       tr.content {
         background-color: rgb(200, 255, 255);
       }
       td.jank0 {
         color: rgb(0, 0, 0);
+        font-weight: bold;
       }
       td.jank1 {
         color: rgb(25, 0, 0);
+        font-weight: bold;
       }
       td.jank2 {
         color: rgb(50, 0, 0);
+        font-weight: bold;
       }
       td.jank3 {
         color: rgb(75, 0, 0);
+        font-weight: bold;
       }
       td.jank4 {
         color: rgb(100, 0, 0);
+        font-weight: bold;
       }
       td.jank5 {
         color: rgb(125, 0, 0);
+        font-weight: bold;
       }
       td.jank6 {
         color: rgb(150, 0, 0);
+        font-weight: bold;
       }
       td.jank7 {
         color: rgb(175, 0, 0);
+        font-weight: bold;
       }
       td.jank8 {
         color: rgb(200, 0, 0);
+        font-weight: bold;
       }
       td.jank9 {
         color: rgb(225, 0, 0);
+        font-weight: bold;
       }
-      td.jank9 {
+      td.jank10 {
         color: rgb(255, 0, 0);
+        font-weight: bold;
       }
     </style>
   </head>
   <body onload="go()">
     <table id="data">
     </table>
   </body>
 </html>
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -1010,17 +1010,17 @@ var NodeListActor = exports.NodeListActo
   marshallPool: function() {
     return this.walker;
   },
 
   // Returns the JSON representation of this object over the wire.
   form: function() {
     return {
       actor: this.actorID,
-      length: this.nodeList.length
+      length: this.nodeList ? this.nodeList.length : 0
     }
   },
 
   /**
    * Get a single node from the node list.
    */
   item: method(function(index) {
     return this.walker.attachElement(this.nodeList[index]);
@@ -1863,34 +1863,34 @@ var WalkerActor = protocol.ActorClass({
         sugs.classes.delete("");
         // Editing the style editor may make the stylesheet have errors and
         // thus the page's elements' styles start changing with a transition.
         // That transition comes from the `moz-styleeditor-transitioning` class.
         sugs.classes.delete("moz-styleeditor-transitioning");
         sugs.classes.delete(HIDDEN_CLASS);
         for (let [className, count] of sugs.classes) {
           if (className.startsWith(completing)) {
-            result.push(["." + className, count]);
+            result.push(["." + className, count, selectorState]);
           }
         }
         break;
 
       case "id":
         if (!query) {
           nodes = this._multiFrameQuerySelectorAll("[id]");
         }
         else {
           nodes = this._multiFrameQuerySelectorAll(query);
         }
         for (let node of nodes) {
           sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
         }
         for (let [id, count] of sugs.ids) {
           if (id.startsWith(completing)) {
-            result.push(["#" + id, count]);
+            result.push(["#" + id, count, selectorState]);
           }
         }
         break;
 
       case "tag":
         if (!query) {
           nodes = this._multiFrameQuerySelectorAll("*");
         }
@@ -1898,19 +1898,30 @@ var WalkerActor = protocol.ActorClass({
           nodes = this._multiFrameQuerySelectorAll(query);
         }
         for (let node of nodes) {
           let tag = node.tagName.toLowerCase();
           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
         }
         for (let [tag, count] of sugs.tags) {
           if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
-            result.push([tag, count]);
+            result.push([tag, count, selectorState]);
           }
         }
+
+        // For state 'tag' (no preceding # or .) and when there's no query (i.e.
+        // only one word) then search for the matching classes and ids
+        if (!query) {
+          result = [
+            ...result,
+            ...this.getSuggestionsForQuery(null, completing, "class").suggestions,
+            ...this.getSuggestionsForQuery(null, completing, "id").suggestions
+          ];
+        }
+
         break;
 
       case "null":
         nodes = this._multiFrameQuerySelectorAll(query);
         for (let node of nodes) {
           sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
           let tag = node.tagName.toLowerCase();
           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
@@ -1930,21 +1941,48 @@ var WalkerActor = protocol.ActorClass({
         // That transition comes from the `moz-styleeditor-transitioning` class.
         sugs.classes.delete("moz-styleeditor-transitioning");
         sugs.classes.delete(HIDDEN_CLASS);
         for (let [className, count] of sugs.classes) {
           className && result.push(["." + className, count]);
         }
     }
 
-    // Sort alphabetically in increaseing order.
-    result = result.sort();
-    // Sort based on count in decreasing order.
-    result = result.sort(function(a, b) {
-      return b[1] - a[1];
+    // Sort by count (desc) and name (asc)
+    result = result.sort((a, b) => {
+      // Computed a sortable string with first the inverted count, then the name
+      let sortA = (10000-a[1]) + a[0];
+      let sortB = (10000-b[1]) + b[0];
+
+      // Prefixing ids, classes and tags, to group results
+      let firstA = a[0].substring(0, 1);
+      let firstB = b[0].substring(0, 1);
+
+      if (firstA === "#") {
+        sortA = "2" + sortA;
+      }
+      else if (firstA === ".") {
+        sortA = "1" + sortA;
+      }
+      else {
+        sortA = "0" + sortA;
+      }
+
+      if (firstB === "#") {
+        sortB = "2" + sortB;
+      }
+      else if (firstB === ".") {
+        sortB = "1" + sortB;
+      }
+      else {
+        sortB = "0" + sortB;
+      }
+
+      // String compare
+      return sortA.localeCompare(sortB);
     });
 
     result.slice(0, 25);
 
     return {
       query: query,
       suggestions: result
     };
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -603,26 +603,30 @@ XPCOMUtils.defineLazyGetter(this, "gCanA
 function getCanStageUpdates() {
   // If background updates are disabled, then just bail out!
   if (!getPref("getBoolPref", PREF_APP_UPDATE_STAGING_ENABLED, false)) {
     LOG("getCanStageUpdates - staging updates is disabled by preference " +
         PREF_APP_UPDATE_STAGING_ENABLED);
     return false;
   }
 
+
+  if (AppConstants.platform == "win" && isServiceInstalled() &&
+      shouldUseService()) {
+    // No need to perform directory write checks, the maintenance service will
+    // be able to write to all directories.
+    LOG("getCanStageUpdates - able to stage updates because we'll use the service");
+    return true;
+  }
+
   // For Gonk, the updater will remount the /system partition to move staged
-  // files into place and it uses the app.update.service.enabled preference for
-  // this purpose.
-  if (AppConstants.platform == "win" || AppConstants.platform == "gonk") {
-    if (getPref("getBoolPref", PREF_APP_UPDATE_SERVICE_ENABLED, false)) {
-      // No need to perform directory write checks, the maintenance service will
-      // be able to write to all directories.
-      LOG("getCanStageUpdates - able to stage updates because we'll use the service");
-      return true;
-    }
+  // files into place.
+  if (AppConstants.platform == "gonk") {
+    LOG("getCanStageUpdates - able to stage updates because this is gonk");
+    return true;
   }
 
   if (!hasUpdateMutex()) {
     LOG("getCanStageUpdates - unable to apply updates because another " +
         "instance of the application is already handling updates for this " +
         "installation.");
     return false;
   }
--- a/toolkit/themes/shared/aboutReader.css
+++ b/toolkit/themes/shared/aboutReader.css
@@ -324,17 +324,18 @@ body.loaded {
   min-width: 300px;
   text-align: start;
   position: absolute;
   left: 48px; /* offset to account for toolbar width */
   z-index: 1000;
   background-color: #fbfbfb;
   visibility: hidden;
   border-radius: 4px;
-  border: 1px 1px 0 1px solid #b5b5b5;
+  border: 1px solid #b5b5b5;
+  border-bottom-width: 0;
   box-shadow: 0 1px 12px #666;
 }
 
 .dropdown-popup > hr {
   display: none;
 }
 
 .open > .dropdown-popup {
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -382,17 +382,17 @@ CopyUpdaterIntoUpdateDir(nsIFile *greDir
   if (!CopyFileIntoUpdateDir(appDir, kUpdaterApp, updateDir))
     return false;
   CopyFileIntoUpdateDir(greDir, kUpdaterINI, updateDir);
 #else
   if (!CopyFileIntoUpdateDir(greDir, kUpdaterBin, updateDir))
     return false;
   CopyFileIntoUpdateDir(appDir, kUpdaterINI, updateDir);
 #endif
-#if defined(XP_UNIX) && !defined(XP_MACOSX) && !defined(ANDROID)
+#if defined(XP_UNIX) && !defined(XP_MACOSX)
   nsCOMPtr<nsIFile> iconDir;
   appDir->Clone(getter_AddRefs(iconDir));
   iconDir->AppendNative(NS_LITERAL_CSTRING("icons"));
   if (!CopyFileIntoUpdateDir(iconDir, kUpdaterPNG, updateDir))
     return false;
 #endif
   // Finally, return the location of the updater binary.
   nsresult rv = updateDir->Clone(getter_AddRefs(updater));