merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 06 Apr 2016 11:59:25 +0200
changeset 348040 68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8
parent 348039 05ce1f2670ae52e928af76df72e78989935141e3 (current diff)
parent 347970 75bef7c5fd91e42e5cfa5337f02f48942b568996 (diff)
child 348057 49d808d13f4fab71b49019905cce3ba6eb6bb31f
child 348076 f4d562358f35db86ab1cf425aa3dd35fe5fcdee1
child 348116 e1a8f719e764e35db6b364213d3efc1d3890e81e
child 348153 769888681ee79fe1daeaf21b830fedceba6ed1b6
child 348165 706c30212c8c07598e5dd5f877612a34db55b59f
child 348170 fc5d4caae79493b840fc2c632dd2e36a21155eaf
child 348182 92a2f9a4698152be2b37d629246018d5c6f00d88
child 348204 baf9b7ddf71c146995d579d0b6b350f6e315282e
child 348243 9f0d28c04e3f16624817eee219b38b8de7b2e378
child 348261 eaa14c9854d3df7ca3918750656e90dcc7b6b62e
child 348262 739aba8e00c5aef8f758a13c4498a1be48615205
child 348275 9b84aa329f555105c72b7c8424f763256ac10143
child 348277 42017d0bcd8227ef2a3709ba433cc43e92ca90d8
child 348289 88d84ce1a079ec224771508c801a4a7864f042b3
child 348291 56c9003c3c6921239e8de52c6f84d3598176ee36
child 348315 4dd67be2805a4fa0fda5f3c8350cb49bf8da8c6b
child 348318 3abd5eb70e19792b2aef6129eda06c95999c2103
child 348319 c388463c76ee4d6831f89e1677255731a7f9b5a4
child 348320 4f7bcdc80a7bad1003f333b6c8781be49d2ce9b2
child 348324 dc5e022e76f0c5733c622326c0077d29fcc1e21b
child 348558 6fbcc3326200ec73b6e8eff462d9b44f0e6975a1
child 348728 3d1132ca685acd096585a1f49612b45c24507d77
child 348754 e474201f5612fe089c78d70ac343b04d1480e485
child 348755 f8c4b609b8b70703ac211efa8dc47ec1247618a3
child 348756 ffce6c9fe7eca324c80c57579eec0d3e064eab46
child 348758 93d79439dd9e6c17fd5890b7d2a074c6445fe574
child 348764 7380900d7a8782e8ac509aeb62f0e40fbff0e4ad
child 348866 153438cc97f53c61d65f0d66962e5cf0b03b4521
child 349220 ef39bc627e3b8eca84fa9a5e078587171a4aed1f
child 349683 3ca681de81c4be56c11c93a3a359fa06d0255023
child 352342 8646a5f959a5cb0bbbc770f3872f4de2a03adf3f
child 353670 d1cc793849d2c320e865e12395e436dfe241241c
child 353683 519cbeae4e6f5d912f6584f443570858c9981e72
child 354213 ed1447d952f777a0c19320f63fb5a9c09046500b
child 354228 6be4ad9c729f5fc5bb26eba18a8cf7c34a1300dd
child 354229 0a98911a10ed96e3b03b7c5bb053f5842d4fc864
child 354593 b1947b652db2974592b2ab73525e167c2b536fa7
child 355262 3ceba3c6050d2c8d0fb549fe0f067faa6a61e076
child 355299 4ef496474dff68c455c4dc2e535bd831ff9c88c4
child 355381 4fe2b198c2866fe2e19681ecb656a1a2dfb14551
child 355406 9add3509f095d5bb098867c0e7102d0c75e9b4cb
push id14738
push userbmo:rail@mozilla.com
push dateWed, 06 Apr 2016 13:35:15 +0000
reviewersmerge
milestone48.0a1
merge fx-team to mozilla-central a=merge
browser/base/content/browser.css
browser/base/content/browser.xul
devtools/client/responsive.html/components/utils/l10n.js
devtools/client/responsive.html/components/utils/moz.build
mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
testing/taskcluster/tasks/branches/base_jobs.yml
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -266,32 +266,32 @@ var gFxAccounts = {
       this.panelUILabel.removeAttribute("disabled");
       this.panelUIAvatar.removeAttribute("disabled");
       this.panelUIIcon.removeAttribute("disabled");
     }
 
     let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
     let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
     let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
-    let signedInTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
+    // The localization string is for the signed in text, but it's the default text as well
+    let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
 
     let updateWithUserData = (userData) => {
       // Window might have been closed while fetching data.
       if (window.closed) {
         return;
       }
 
       // Reset the button to its original state.
       this.panelUILabel.setAttribute("label", defaultLabel);
-      this.panelUIStatus.removeAttribute("tooltiptext");
+      this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
       this.panelUIFooter.removeAttribute("fxastatus");
       this.panelUIFooter.removeAttribute("fxaprofileimage");
       this.panelUIAvatar.style.removeProperty("list-style-image");
       let showErrorBadge = false;
-
       if (!this._inCustomizationMode && userData) {
         // At this point we consider the user as logged-in (but still can be in an error state)
         if (this.loginFailed) {
           let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1);
           this.panelUIFooter.setAttribute("fxastatus", "error");
           this.panelUILabel.setAttribute("label", errorLabel);
           this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
           showErrorBadge = true;
@@ -300,17 +300,16 @@ var gFxAccounts = {
           this.panelUIFooter.setAttribute("fxastatus", "error");
           this.panelUIFooter.setAttribute("unverified", "true");
           this.panelUILabel.setAttribute("label", unverifiedLabel);
           this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
           showErrorBadge = true;
         } else {
           this.panelUIFooter.setAttribute("fxastatus", "signedin");
           this.panelUILabel.setAttribute("label", userData.email);
-          this.panelUIStatus.setAttribute("tooltiptext", signedInTooltiptext);
         }
         if (profileInfoEnabled) {
           this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
         }
       }
       if (showErrorBadge) {
         gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
       } else {
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -442,35 +442,39 @@ toolbar:not(#TabsToolbar) > #personal-bo
   display: none;
 }
 
 /* ::::: location bar ::::: */
 #urlbar {
   -moz-binding: url(chrome://browser/content/urlbarBindings.xml#urlbar);
 }
 
-.ac-url-text:-moz-locale-dir(rtl),
-.ac-title:-moz-locale-dir(rtl) > description {
+/* Always show URLs LTR. */
+.ac-url-text:-moz-locale-dir(rtl) {
   direction: ltr !important;
 }
 
-/* For results that are actions, their description text is shown instead of
-   the URL - this needs to follow the locale's direction, unlike URLs. */
-panel:not([noactions]) > richlistbox > richlistitem.overridable-action:-moz-locale-dir(rtl) > .ac-url-box {
-  direction: rtl;
-}
-
-panel[noactions] > richlistbox > richlistitem.overridable-action > .ac-url-box > .ac-url > .ac-action-text,
-panel[noactions] > richlistbox > richlistitem.overridable-action > .ac-url-box > .ac-action-icon {
+/* For non-action items, hide the action text; for action items, hide the URL
+   text. */
+.ac-url[actiontype],
+.ac-action:not([actiontype]) {
   visibility: collapse;
 }
 
-panel[noactions] > richlistbox > richlistitem.overridable-action > .ac-url-box > .ac-url > .ac-url-text {
+/* For action items in a noactions popup, show the URL text and hide the action
+   text and type icon. */
+#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-url {
   visibility: visible;
 }
+#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-action {
+  visibility: collapse;
+}
+#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-type-icon {
+  list-style-image: none;
+}
 
 #urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
   display: none;
 }
 
 #PopupAutoComplete {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup");
 }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -135,17 +135,17 @@
 
     <!-- for search and content formfill/pw manager -->
     <panel type="autocomplete" id="PopupAutoComplete" noautofocus="true" hidden="true"/>
 
     <!-- for search with one-off buttons -->
     <panel type="autocomplete" id="PopupSearchAutoComplete" noautofocus="true" hidden="true"/>
 
     <!-- for url bar autocomplete -->
-    <panel type="autocomplete-richlistbox" id="PopupAutoCompleteRichResult" noautofocus="true" hidden="true">
+    <panel type="autocomplete-richlistbox" id="PopupAutoCompleteRichResult" noautofocus="true" hidden="true" flip="none">
 #ifdef NIGHTLY_BUILD
       <hbox id="urlbar-search-footer" flex="1" align="stretch" pack="end">
         <button id="urlbar-search-settings" label="&changeSearchSettings.button;"
                 oncommand="BrowserUITelemetry.countSearchSettingsEvent('urlbar'); openPreferences('paneSearch')"/>
       </hbox>
 #endif
     </panel>
 
--- a/browser/base/content/test/general/browser_action_keyword.js
+++ b/browser/base/content/test/general/browser_action_keyword.js
@@ -44,30 +44,29 @@ add_task(function*() {
   is(result.getAttribute("actiontype"), "keyword", "Expect correct `actiontype` attribute");
   is(result.getAttribute("title"), "example.com", "Expect correct title");
 
   // We need to make a real URI out of this to ensure it's normalised for
   // comparison.
   let uri = NetUtil.newURI(result.getAttribute("url"));
   is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=something", input: "keyword something"}).spec, "Expect correct url");
 
-  is_element_visible(result._title, "Title element should be visible");
-  is(result._title.childNodes.length, 1, "Title element should have 1 child");
-  is(result._title.childNodes[0].nodeName, "#text", "That child should be a text node");
-  is(result._title.childNodes[0].data, "example.com", "Node should contain the name of the bookmark");
+  let titleHbox = result._titleText.parentNode.parentNode;
+  ok(titleHbox.classList.contains("ac-title"), "Title hbox element sanity check");
+  is_element_visible(titleHbox, "Title element should be visible");
+  is(result._titleText.textContent, "example.com: something", "Node should contain the name of the bookmark and query");
 
-  is_element_visible(result._extraBox, "Extra element should be visible");
-  is(result._extra.childNodes.length, 1, "Title element should have 1 child");
-  is(result._extra.childNodes[0].nodeName, "span", "That child should be a span node");
-  let span = result._extra.childNodes[0];
-  is(span.childNodes.length, 1, "span element should have 1 child");
-  is(span.childNodes[0].nodeName, "#text", "That child should be a text node");
-  is(span.childNodes[0].data, "something", "Node should contain the query for the keyword");
+  let urlHbox = result._urlText.parentNode.parentNode;
+  ok(urlHbox.classList.contains("ac-url"), "URL hbox element sanity check");
+  is_element_hidden(urlHbox, "URL element should be hidden");
 
-  is_element_hidden(result._url, "URL element should be hidden");
+  let actionHbox = result._actionText.parentNode.parentNode;
+  ok(actionHbox.classList.contains("ac-action"), "Action hbox element sanity check");
+  is_element_visible(actionHbox, "Action element should be visible");
+  is(result._actionText.textContent, "", "Action text should be empty");
 
   // Click on the result
   info("Normal click on result");
   let tabPromise = promiseTabLoadEvent(tab);
   EventUtils.synthesizeMouseAtCenter(result, {});
   yield tabPromise;
   is(tab.linkedBrowser.currentURI.spec, "http://example.com/?q=something", "Tab should have loaded from clicking on result");
 
--- a/browser/base/content/test/general/browser_action_keyword_override.js
+++ b/browser/base/content/test/general/browser_action_keyword_override.js
@@ -15,21 +15,33 @@ add_task(function*() {
   registerCleanupFunction(function* () {
     yield PlacesUtils.bookmarks.remove(bm);
   });
 
   yield promiseAutocompleteResultPopup("keyword search");
   let result = gURLBar.popup.richlistbox.children[0];
 
   info("Before override");
-  is_element_hidden(result._url, "URL element should be hidden");
-  is_element_visible(result._extraBox, "Extra element should be visible");
+  let titleHbox = result._titleText.parentNode.parentNode;
+  ok(titleHbox.classList.contains("ac-title"), "Title hbox element sanity check");
+  is_element_visible(titleHbox, "Title element should be visible");
+
+  let urlHbox = result._urlText.parentNode.parentNode;
+  ok(urlHbox.classList.contains("ac-url"), "URL hbox element sanity check");
+  is_element_hidden(urlHbox, "URL element should be hidden");
+
+  let actionHbox = result._actionText.parentNode.parentNode;
+  ok(actionHbox.classList.contains("ac-action"), "Action hbox element sanity check");
+  is_element_visible(actionHbox, "Action element should be visible");
+  is(result._actionText.textContent, "", "Action text should be empty");
 
   info("During override");
   EventUtils.synthesizeKey("VK_SHIFT" , { type: "keydown" });
-  is_element_hidden(result._url, "URL element should be hidden");
-  is_element_visible(result._extraBox, "Extra element should be visible");
+  is_element_visible(titleHbox, "Title element should be visible");
+  is_element_hidden(urlHbox, "URL element should be hidden");
+  is_element_visible(actionHbox, "Action element should be visible");
+  is(result._actionText.textContent, "", "Action text should be empty");
 
   EventUtils.synthesizeKey("VK_SHIFT" , { type: "keyup" });
 
   gURLBar.popup.hidePopup();
   yield promisePopupHidden(gURLBar.popup);
 });
--- a/browser/base/content/test/general/browser_autocomplete_no_title.js
+++ b/browser/base/content/test/general/browser_autocomplete_no_title.js
@@ -13,14 +13,14 @@ add_task(function*() {
   yield promiseTabLoaded(tab);
 
   let uri = NetUtil.newURI("http://bug1060642.example.com/beards/are/pretty/great");
   yield PlacesTestUtils.addVisits([{uri: uri, title: ""}]);
 
   yield promiseAutocompleteResultPopup("bug1060642");
   ok(gURLBar.popup.richlistbox.children.length > 1, "Should get at least 2 results");
   let result = gURLBar.popup.richlistbox.children[1];
-  is(result._title.textContent, "bug1060642.example.com", "Result title should be as expected");
+  is(result._titleText.textContent, "bug1060642.example.com", "Result title should be as expected");
 
   gURLBar.popup.hidePopup();
   yield promisePopupHidden(gURLBar.popup);
   gBrowser.removeTab(tab);
 });
--- a/browser/base/content/test/general/browser_autocomplete_tag_star_visibility.js
+++ b/browser/base/content/test/general/browser_autocomplete_tag_star_visibility.js
@@ -74,32 +74,34 @@ add_task(function*() {
     },
     input: "^ tagtest5",
     expected: {
       type: "tag",
       typeImageVisible: false,
     },
   }];
 
-
   for (let testcase of testcases) {
     info(`Test case: ${testcase.description}`);
 
     yield addTagItem(testcase.tagName);
     for (let prefName of Object.keys(testcase.prefs)) {
       Services.prefs.setBoolPref(`browser.urlbar.${prefName}`, testcase.prefs[prefName]);
     }
 
     yield promiseAutocompleteResultPopup(testcase.input);
     let result = gURLBar.popup.richlistbox.children[1];
     ok(result && !result.collasped, "Should have result");
 
     is(result.getAttribute("type"), testcase.expected.type, "Result should have expected type");
+
+    let typeIconStyle = window.getComputedStyle(result._typeIcon);
+    let imageURL = typeIconStyle.listStyleImage;
     if (testcase.expected.typeImageVisible) {
-      is_element_visible(result._typeImage, "Type image should be visible");
+      ok(/^url\(.+\)$/.test(imageURL), "Type image should be visible");
     } else {
-      is_element_hidden(result._typeImage, "Type image should be hidden");
+      is(imageURL, "none", "Type image should be hidden");
     }
 
     gURLBar.popup.hidePopup();
     yield promisePopupHidden(gURLBar.popup);
   }
 });
--- a/browser/base/content/test/general/browser_fxaccounts.js
+++ b/browser/base/content/test/general/browser_fxaccounts.js
@@ -75,17 +75,17 @@ add_task(function* test_nouser() {
   let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem");
   Services.obs.notifyObservers(null, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, null);
   yield promiseUpdateDone;
 
   // Check the world - the FxA footer area is visible as it is offering a signin.
   Assert.ok(isFooterVisible())
 
   Assert.equal(panelUILabel.getAttribute("label"), panelUIStatus.getAttribute("defaultlabel"));
-  Assert.ok(!panelUIStatus.hasAttribute("tooltiptext"), "no tooltip when signed out");
+  Assert.equal(panelUIStatus.getAttribute("tooltiptext"), panelUIStatus.getAttribute("signedinTooltiptext"));
   Assert.ok(!panelUIFooter.hasAttribute("fxastatus"), "no fxsstatus when signed out");
   Assert.ok(!panelUIFooter.hasAttribute("fxaprofileimage"), "no fxaprofileimage when signed out");
 
   let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
   panelUIStatus.click();
   yield promisePreferencesOpened;
 });
 
--- a/browser/base/content/test/general/browser_search_favicon.js
+++ b/browser/base/content/test/general/browser_search_favicon.js
@@ -35,25 +35,26 @@ add_task(function*() {
   yield promiseTabLoaded(gBrowser.selectedTab);
 
   // The first autocomplete result has the action searchengine, while
   // the second result is the "search favicon" element.
   yield promiseAutocompleteResultPopup("foo");
   let result = gURLBar.popup.richlistbox.children[1];
 
   isnot(result, null, "Expect a search result");
-  is(result.getAttribute("type"), "search favicon", "Expect correct `type` attribute");
+  is(result.getAttribute("type"), "action searchengine favicon", "Expect correct `type` attribute");
 
-  is_element_visible(result._title, "Title element should be visible");
-  is_element_visible(result._extraBox, "Extra box element should be visible");
+  let titleHbox = result._titleText.parentNode.parentNode;
+  ok(titleHbox.classList.contains("ac-title"), "Title hbox sanity check");
+  is_element_visible(titleHbox, "Title element should be visible");
 
-  is(result._extraBox.pack, "start", "Extra box element should start after the title");
-  let iconElem = result._extraBox.nextSibling;
-  is_element_visible(iconElem,
-                     "The element containing the magnifying glass icon should be visible");
-  ok(iconElem.classList.contains("ac-result-type-keyword"),
-     "That icon should have the same class use for `keyword` results");
+  let urlHbox = result._urlText.parentNode.parentNode;
+  ok(urlHbox.classList.contains("ac-url"), "URL hbox sanity check");
+  is_element_hidden(urlHbox, "URL element should be hidden");
 
-  is_element_visible(result._url, "URL element should be visible");
-  is(result._url.textContent, "Search with SearchEngine");
+  let actionHbox = result._actionText.parentNode.parentNode;
+  ok(actionHbox.classList.contains("ac-action"), "Action hbox sanity check");
+  is_element_hidden(actionHbox, "Action element should be hidden because it is not selected");
+  // \u2014 == em dash
+  is(result._actionText.textContent, "\u2014Search with SearchEngine", "Action text should be as expected");
 
   gBrowser.removeCurrentTab();
 });
--- a/browser/base/content/test/general/browser_urlbarDecode.js
+++ b/browser/base/content/test/general/browser_urlbarDecode.js
@@ -88,10 +88,12 @@ function* checkInput(inputStr) {
   Assert.equal(item.getAttribute("text"), inputStr, "text");
 
   let itemTypeStr = item.getAttribute("type");
   let itemTypes = itemTypeStr.split(" ").sort();
   Assert.equal(itemTypes.toString(),
                ["action", "heuristic", "visiturl"].toString(),
                "type");
 
-  Assert.equal(item._title.textContent, "Visit " + inputStr.replace("\\","/"), "Visible title");
+  Assert.equal(item._titleText.textContent, inputStr.replace("\\","/"), "Visible title");
+  // \u2014 == em dash
+  Assert.equal(item._actionText.textContent, "\u2014Visit", "Visible action");
 }
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1334,16 +1334,51 @@ file, You can obtain one at http://mozil
             this._showSearchSuggestionsNotification();
           }
 
           this._openAutocompletePopup(aInput, aElement);
           ]]>
         </body>
       </method>
 
+      <method name="_openAutocompletePopup">
+        <parameter name="aInput"/>
+        <parameter name="aElement"/>
+        <body><![CDATA[
+          if (this.mPopupOpen) {
+            return;
+          }
+
+          this.mInput = aInput;
+          this.selectedIndex = -1;
+          this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
+          this.invalidate();
+
+          var rect = window.document.documentElement.getBoundingClientRect();
+          var width = rect.right - rect.left;
+          this.setAttribute("width", width);
+
+          // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
+          var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
+          this.style.direction = popupDirection;
+
+          // Move left margin to the window border.
+          let elementRect = aElement.getBoundingClientRect();
+          this.style.marginLeft = "-" + (elementRect.left - rect.left) + "px";
+
+          // Position the popup below the navbar.  To get the y-coordinate,
+          // which is an offset from the bottom of the input, subtract the
+          // bottom of the navbar from the buttom of the input.
+          let yOffset =
+            document.getElementById("nav-bar").getBoundingClientRect().bottom -
+            aInput.getBoundingClientRect().bottom;
+          this.openPopup(aElement, "after_start", 0, yOffset, false, false);
+        ]]></body>
+      </method>
+
       <method name="_updateFooterVisibility">
         <body>
           <![CDATA[
           this.footer.collapsed = this._matchCount == 0;
           ]]>
         </body>
       </method>
 
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -19,16 +19,17 @@
         <toolbarbutton id="PanelUI-update-status"
                        oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);"
                        wrap="true"
                        hidden="true"/>
         <hbox id="PanelUI-footer-fxa">
           <hbox id="PanelUI-fxa-status"
                 defaultlabel="&fxaSignIn.label;"
                 signedinTooltiptext="&fxaSignedIn.tooltip;"
+                tooltiptext="&fxaSignedIn.tooltip;"
                 errorlabel="&fxaSignInError.label;"
                 unverifiedlabel="&fxaUnverified.label;"
                 onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();">
             <image id="PanelUI-fxa-avatar"/>
             <toolbarbutton id="PanelUI-fxa-label"
                            fxabrandname="&syncBrand.fxAccount.label;"/>
           </hbox>
           <toolbarseparator/>
--- a/browser/components/sessionstore/SessionSaver.jsm
+++ b/browser/components/sessionstore/SessionSaver.jsm
@@ -206,26 +206,28 @@ var SessionSaverInternal = {
           break;
         }
 
         delete state._closedWindows[i]._shouldRestore;
         state.windows.unshift(state._closedWindows.pop());
       }
     }
 
-    // If this is the final write on a clean shutdown, and the user changed
-    // their cookie preferences to "Keep until I close Firefox", then we
-    // should remove all cookies. Check "resume_session_once" so we keep
-    // cookies when restarting due to a Firefox update.
-    if (RunState.isClosing &&
-        Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
-          Services.cookies.ACCEPT_SESSION &&
-        !Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
-      for (let window of state.windows) {
-        delete window.cookies;
+    // Clear all cookies on clean shutdown according to user preferences
+    if (RunState.isClosing) {
+      let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
+                            Services.cookies.ACCEPT_SESSION;
+      let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
+                            Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
+      let restart = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
+      // Don't clear cookies when restarting
+      if ((expireCookies || sanitizeCookies) && !restart) {
+        for (let window of state.windows) {
+          delete window.cookies;
+        }
       }
     }
 
     stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
     return this._writeState(state);
   },
 
   /**
--- a/browser/components/syncedtabs/TabListComponent.js
+++ b/browser/components/syncedtabs/TabListComponent.js
@@ -76,21 +76,18 @@ TabListComponent.prototype = {
   onFilterFocus() {
     this._store.focusInput();
   },
 
   onFilterBlur() {
     this._store.blurInput();
   },
 
-  onSelectRow(position, id) {
+  onSelectRow(position) {
     this._store.selectRow(position[0], position[1]);
-    if (id) {
-      this._store.toggleBranch(id);
-    }
   },
 
   onMoveSelectionDown() {
     this._store.moveSelectionDown();
   },
 
   onMoveSelectionUp() {
     this._store.moveSelectionUp();
@@ -102,17 +99,17 @@ TabListComponent.prototype = {
 
   onBookmarkTab(uri, title) {
     this._window.top.PlacesCommandHook
       .bookmarkLink(this._window.top.PlacesUtils.bookmarksMenuFolderId, uri, title)
       .catch(Cu.reportError);
   },
 
   onOpenTab(url, where, params) {
-    this._window.openLinkIn(url, where, params);
+    this._window.openUILinkIn(url, where, params);
   },
 
   onCopyTabLocation(url) {
     this._clipboardHelper.copyString(url);
   },
 
   onSyncRefresh() {
     this._SyncedTabs.syncTabs(true);
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -250,47 +250,44 @@ TabListView.prototype = {
     let itemNode = this._findParentItemNode(event.target);
     if (!itemNode) {
       return;
     }
 
     if (itemNode.classList.contains("tab")) {
       let url = itemNode.dataset.url;
       if (url) {
-        this.props.onOpenTab(url, event);
+        this.onOpenSelected(url, event);
       }
     }
 
     if (event.target.classList.contains("item-twisty-container")) {
       this.props.onToggleBranch(itemNode.dataset.id);
       return;
     }
 
-    this._selectRow(itemNode);
-  },
-
-  _selectRow(itemNode) {
-    this.props.onSelectRow(this._getSelectionPosition(itemNode), itemNode.dataset.id);
+    let position = this._getSelectionPosition(itemNode);
+    this.props.onSelectRow(position);
   },
 
   /**
    * Handle a keydown event on the list box.
    * @param {Event} event - Triggering event.
    */
   onKeyDown(event) {
     if (event.keyCode == this._window.KeyEvent.DOM_VK_DOWN) {
       event.preventDefault();
       this.props.onMoveSelectionDown();
     } else if (event.keyCode == this._window.KeyEvent.DOM_VK_UP) {
       event.preventDefault();
       this.props.onMoveSelectionUp();
     } else if (event.keyCode == this._window.KeyEvent.DOM_VK_RETURN) {
       let selectedNode = this.container.querySelector('.item.selected');
       if (selectedNode.dataset.url) {
-        this.props.onOpenTab(selectedNode.dataset.url, event);
+        this.onOpenSelected(selectedNode.dataset.url, event);
       } else if (selectedNode) {
         this.props.onToggleBranch(selectedNode.dataset.id);
       }
     }
   },
 
   onBookmarkTab() {
     let item = this._getSelectedTabNode();
@@ -302,17 +299,22 @@ TabListView.prototype = {
 
   onCopyTabLocation() {
     let item = this._getSelectedTabNode();
     if (item) {
       this.props.onCopyTabLocation(item.dataset.url);
     }
   },
 
-  onOpenSelected(event) {
+  onOpenSelected(url, event) {
+    let where = getChromeWindow(this._window).whereToOpenLink(event);
+    this.props.onOpenTab(url, where, {});
+  },
+
+  onOpenSelectedFromContextMenu(event) {
     let item = this._getSelectedTabNode();
     if (item) {
       let where = event.target.getAttribute("where");
       let params = {
         private: event.target.hasAttribute("private"),
       };
       this.props.onOpenTab(item.dataset.url, where, params);
     }
@@ -417,17 +419,17 @@ TabListView.prototype = {
 
   handleContentContextMenuCommand(event) {
     let id = event.target.getAttribute("id");
     switch (id) {
       case "syncedTabsOpenSelected":
       case "syncedTabsOpenSelectedInTab":
       case "syncedTabsOpenSelectedInWindow":
       case "syncedTabsOpenSelectedInPrivateWindow":
-        this.onOpenSelected(event);
+        this.onOpenSelectedFromContextMenu(event);
         break;
       case "syncedTabsBookmarkSelected":
         this.onBookmarkTab();
         break;
       case "syncedTabsCopySelected":
         this.onCopyTabLocation();
         break;
       case "syncedTabsRefresh":
@@ -447,17 +449,18 @@ TabListView.prototype = {
   handleContextMenu(event) {
     let menu;
 
     if (event.target == this.tabsFilter) {
       menu = getTabsFilterContextMenu(this._window);
     } else {
       let itemNode = this._findParentItemNode(event.target);
       if (itemNode) {
-        this._selectRow(itemNode);
+        let position = this._getSelectionPosition(itemNode);
+        this.props.onSelectRow(position);
       }
       menu = getContextMenu(this._window);
       this.adjustContextMenu(menu);
     }
 
     menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
   },
 
--- a/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
+++ b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
@@ -359,16 +359,17 @@ function checkItem(node, item) {
     Assert.equal(node.dataset.id, item.id,
       "Node's ID should match item ID");
   }
 }
 
 function* testContextMenu(syncedTabsDeckComponent, contextSelector, triggerSelector, menuSelectors) {
   let contextMenu = document.querySelector(contextSelector);
   let triggerElement = syncedTabsDeckComponent._window.document.querySelector(triggerSelector);
+  let isClosed = triggerElement.classList.contains("closed");
 
   let promisePopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
 
   let chromeWindow = triggerElement.ownerDocument.defaultView.top;
   let rect = triggerElement.getBoundingClientRect();
   let contentRect = chromeWindow.SidebarUI.browser.getBoundingClientRect();
   // The offsets in `rect` are relative to the content window, but
   // `synthesizeMouseAtPoint` calls `nsIDOMWindowUtils.sendMouseEvent`,
@@ -378,16 +379,18 @@ function* testContextMenu(syncedTabsDeck
   let offsetX = contentRect.x + rect.x + (rect.width / 2);
   let offsetY = contentRect.y + rect.y + (rect.height / 4);
 
   yield EventUtils.synthesizeMouseAtPoint(offsetX, offsetY, {
     type: "contextmenu",
     button: 2,
   }, chromeWindow);
   yield promisePopupShown;
+  is(triggerElement.classList.contains("closed"), isClosed,
+    "Showing the context menu shouldn't toggle the tab list");
   checkChildren(contextMenu, menuSelectors);
 
   let promisePopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
   contextMenu.hidePopup();
   yield promisePopupHidden;
 }
 
 function checkChildren(node, selectors) {
--- a/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
+++ b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
@@ -72,17 +72,17 @@ add_task(function* testActions() {
   };
   let windowMock = {
     top: {
       PlacesCommandHook: {
         bookmarkLink() { return Promise.resolve(); }
       },
       PlacesUtils: { bookmarksMenuFolderId: "id" }
     },
-    openLinkIn() {}
+    openUILinkIn() {}
   };
   let component = new TabListComponent({
     window: windowMock, store, View: null, SyncedTabs,
     clipboardHelper: clipboardHelperMock});
 
   sinon.stub(store, "getData");
   component.onFilter("query");
   Assert.ok(store.getData.calledWith("query"));
@@ -95,40 +95,39 @@ add_task(function* testActions() {
   component.onFilterFocus();
   Assert.ok(store.focusInput.called);
 
   sinon.stub(store, "blurInput");
   component.onFilterBlur();
   Assert.ok(store.blurInput.called);
 
   sinon.stub(store, "selectRow");
-  sinon.stub(store, "toggleBranch");
-  component.onSelectRow([-1, -1], "foo-id");
+  component.onSelectRow([-1, -1]);
   Assert.ok(store.selectRow.calledWith(-1, -1));
-  Assert.ok(store.toggleBranch.calledWith("foo-id"));
 
   sinon.stub(store, "moveSelectionDown");
   component.onMoveSelectionDown();
   Assert.ok(store.moveSelectionDown.called);
 
   sinon.stub(store, "moveSelectionUp");
   component.onMoveSelectionUp();
   Assert.ok(store.moveSelectionUp.called);
 
+  sinon.stub(store, "toggleBranch");
   component.onToggleBranch("foo-id");
-  Assert.ok(store.toggleBranch.secondCall.calledWith("foo-id"));
+  Assert.ok(store.toggleBranch.calledWith("foo-id"));
 
   sinon.spy(windowMock.top.PlacesCommandHook, "bookmarkLink");
   component.onBookmarkTab("uri", "title");
   Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][1], "uri");
   Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][2], "title");
 
-  sinon.spy(windowMock, "openLinkIn");
+  sinon.spy(windowMock, "openUILinkIn");
   component.onOpenTab("uri", "where", "params");
-  Assert.ok(windowMock.openLinkIn.calledWith("uri", "where", "params"));
+  Assert.ok(windowMock.openUILinkIn.calledWith("uri", "where", "params"));
 
   sinon.spy(clipboardHelperMock, "copyString");
   component.onCopyTabLocation("uri");
   Assert.ok(clipboardHelperMock.copyString.calledWith("uri"));
 
   sinon.stub(SyncedTabs, "syncTabs");
   component.onSyncRefresh();
   Assert.ok(SyncedTabs.syncTabs.calledWith(true));
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1091,75 +1091,114 @@ notification[value="translation"] button
 notification[value="translation"] menulist > .menulist-dropmarker {
   display: block;
 }
 
 #treecolAutoCompleteImage {
   max-width : 36px;
 }
 
-.ac-result-type-bookmark,
+#PopupAutoCompleteRichResult {
+  /* The awesomebar popup should open just below the navbar bottom border. */
+  margin-top: 1px;
+}
+
+.autocomplete-richlistbox {
+  padding: 4px;
+}
+
+.autocomplete-richlistitem {
+  height: 30px;
+  min-height: 30px;
+  font: message-box;
+  border-radius: 2px;
+  border: 1px solid transparent;
+}
+
+.autocomplete-richlistitem[selected=true] {
+  background-color: Highlight;
+}
+
+.ac-title {
+  font-size: 14px;
+}
+
+.ac-tags {
+  font-size: 12px;
+}
+
+html|span.ac-tag {
+  background-color: MenuText;
+  color: Menu;
+  border-radius: 2px;
+  border: 1px solid transparent;
+  padding: 0 1px;
+}
+
+.ac-url,
+.ac-action {
+  font-size: 12px;
+  color: -moz-nativehyperlinktext;
+}
+
+.ac-title[selected=true],
+.ac-url[selected=true],
+.ac-action[selected=true] {
+  color: inherit !important;
+}
+
+.ac-tags-text[selected] > html|span.ac-tag {
+  background-color: HighlightText;
+  color: Highlight;
+}
+
+.ac-type-icon[type=bookmark] {
+  list-style-image: url("chrome://browser/skin/urlbar-star.svg#star");
+}
+
+.ac-type-icon[type=bookmark][selected][current] {
+  list-style-image: url("chrome://browser/skin/urlbar-star.svg#star-inverted");
+}
+
 .autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/autocomplete-star.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-result-type-keyword,
-.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage),
-richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-site-icon {
+.ac-type-icon[type=keyword],
+.ac-site-icon[type=searchengine],
+.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage) {
   list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
-  width: 16px;
-  height: 16px;
 }
 
-.ac-result-type-keyword[selected="true"],
-.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected),
-richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-title-box > .ac-site-icon {
+.ac-type-icon[type=keyword][selected],
+.ac-site-icon[type=searchengine][selected],
+.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected) {
   list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon-inverted);
 }
 
-.ac-result-type-tag,
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-comment,
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description,
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
-  font-size: 1.05em;
-}
-
-.ac-extra > .ac-comment {
-  font-size: inherit;
+.ac-type-icon[type=switchtab] {
+  list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
 }
 
-.ac-url-text,
-.ac-action-text {
-  color: -moz-nativehyperlinktext;
-  font-size: 0.9em;
-}
-
-richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
-  list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-  padding: 0 3px;
+.ac-type-icon[type=switchtab][selected] {
+  list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
-.ac-comment[selected="true"],
-.ac-url-text[selected="true"],
-.ac-action-text[selected="true"] {
-  color: inherit !important;
-}
-
 .autocomplete-treebody::-moz-tree-cell-text(suggesthint, treecolAutoCompleteComment),
 .autocomplete-treebody::-moz-tree-cell-text(suggestfirst, treecolAutoCompleteComment) {
   color: GrayText;
   font-size: smaller;
 }
 
 .autocomplete-treebody::-moz-tree-cell(suggesthint) {
   border-top: 1px solid GrayText;
--- a/browser/themes/linux/devedition.css
+++ b/browser/themes/linux/devedition.css
@@ -94,8 +94,13 @@
 }
 
 /* Fix the bad-looking text-shadow in the sidebar header: */
 .sidebar-header,
 #sidebar-header {
   text-shadow: none;
 }
 
+.ac-type-icon {
+  /* Left-align the type icon in awesomebar popup results with the icon in the
+     urlbar. */
+  -moz-margin-start: 11px;
+}
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1707,116 +1707,148 @@ toolbar .toolbarbutton-1 > .toolbarbutto
 }
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult {
-  margin-top: 2px;
-}
-
 %include ../shared/urlbarSearchSuggestionsNotification.inc.css
 
 /* ----- AUTOCOMPLETE ----- */
 
 #treecolAutoCompleteImage {
   max-width: 36px;
 }
 
-.ac-result-type-bookmark,
+.autocomplete-richlistbox {
+  padding: 4px;
+}
+
+.autocomplete-richlistitem {
+  height: 30px;
+  min-height: 30px;
+  font: message-box;
+  border-radius: 2px;
+  border: 1px solid transparent;
+}
+
+.autocomplete-richlistitem[selected] {
+  background-color: hsl(210, 80%, 52%);
+}
+
+.ac-title {
+  font-size: 14px;
+  color: hsl(0, 0%, 0%);
+}
+
+html|span.ac-emphasize-text-title {
+  color: hsl(0, 0%, 50%);
+}
+
+.ac-tags {
+  font-size: 12px;
+}
+
+html|span.ac-tag {
+  background-color: hsl(216, 0%, 88%);
+  color: hsl(0, 0%, 0%);
+  border-radius: 2px;
+  border: 1px solid transparent;
+  padding: 0 1px;
+}
+
+html|span.ac-emphasize-text-tag {
+  color: hsl(0, 0%, 50%);
+}
+
+.ac-url {
+  font-size: 12px;
+  color: hsl(210, 77%, 47%);
+}
+
+html|span.ac-emphasize-text-url {
+  color: hsl(210, 86%, 64%);
+}
+
+.ac-action {
+  font-size: 12px;
+  color: hsl(178, 100%, 28%);
+}
+
+html|span.ac-title-urlaction-separator {
+  color: hsl(0, 0%, 50%);
+}
+
+.ac-title[selected],
+.ac-url[selected],
+.ac-action[selected],
+.ac-title-text[selected] > html|span.ac-emphasize-text,
+.ac-url-text[selected] > html|span.ac-emphasize-text,
+.ac-action-text[selected] > html|span.ac-emphasize-text,
+.ac-url-text[selected] > html|span.ac-title-urlaction-separator,
+.ac-action-text[selected] > html|span.ac-title-urlaction-separator {
+  color: hsl(0, 0%, 100%);
+}
+
+.ac-tags-text[selected] > html|span.ac-tag {
+  background-color: hsl(0, 0%, 100%);
+  color: hsl(210, 80%, 40%);
+}
+
+.ac-tags-text[selected] > html|span.ac-tag > html|span.ac-emphasize-text-tag {
+  color: hsl(210, 80%, 52%);
+}
+
+.ac-type-icon[type=bookmark] {
+  list-style-image: url("chrome://browser/skin/urlbar-star.svg#star");
+}
+
 .autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/autocomplete-star.png");
   -moz-image-region: rect(0, 16px, 16px, 0);
 }
 
-richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark,
 .autocomplete-treebody::-moz-tree-image(selected, current, bookmark, treecolAutoCompleteImage) {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
-.ac-result-type-keyword,
-.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage),
-richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-site-icon {
+.ac-type-icon[type=bookmark][selected][current] {
+  list-style-image: url("chrome://browser/skin/urlbar-star.svg#star-inverted");
+}
+
+.ac-type-icon[type=keyword],
+.ac-site-icon[type=searchengine],
+.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage) {
   list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
-  width: 16px;
-  height: 16px;
-}
-
-.ac-result-type-keyword[selected="true"],
-.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected),
-richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-title-box > .ac-site-icon {
+}
+
+.ac-type-icon[type=keyword][selected],
+.ac-site-icon[type=searchengine][selected],
+.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected) {
   list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon-inverted);
 }
 
-.ac-result-type-tag,
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
-  width: 16px;
-  height: 16px;
-}
-
-.ac-extra > .ac-comment {
-  font-size: inherit;
-}
-
-.ac-url-text,
-.ac-action-text {
-  font: message-box;
-  color: -moz-nativehyperlinktext;
-}
-
-richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
-  list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-  -moz-image-region: rect(0, 16px, 11px, 0);
-  padding: 0 3px;
-}
-
-richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-  -moz-image-region: rect(11px, 16px, 22px, 0);
-}
-
-@media (min-resolution: 1.1dppx) {
-  .ac-result-type-bookmark {
-    list-style-image: url("chrome://browser/skin/places/autocomplete-star@2x.png");
-    -moz-image-region: rect(0, 32px, 32px, 0);
-  }
-
-  richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark {
-    list-style-image: url("chrome://browser/skin/places/autocomplete-star@2x.png");
-    -moz-image-region: rect(0, 64px, 32px, 32px);
-  }
-
-  .ac-result-type-tag {
-    list-style-image: url("chrome://browser/skin/places/tag@2x.png");
-  }
-
-  richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
-    list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
-    -moz-image-region: rect(0, 32px, 22px, 0);
-    width: 22px;
-  }
-
-  richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-    -moz-image-region: rect(22px, 32px, 44px, 0);
-  }
+}
+
+.ac-type-icon[type=switchtab] {
+  list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
+}
+
+.ac-type-icon[type=switchtab][selected] {
+  list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
-.ac-comment[selected="true"],
-.ac-url-text[selected="true"],
-.ac-action-text[selected="true"] {
-  color: inherit !important;
-}
-
 .autocomplete-treebody::-moz-tree-cell-text(suggesthint, treecolAutoCompleteComment),
 .autocomplete-treebody::-moz-tree-cell-text(suggestfirst, treecolAutoCompleteComment)
 {
   color: GrayText;
   font-size: smaller;
 }
 
 .autocomplete-treebody::-moz-tree-cell(suggesthint) {
--- a/browser/themes/osx/devedition.css
+++ b/browser/themes/osx/devedition.css
@@ -116,8 +116,14 @@
   -moz-image-region: rect(0, 64px, 16px, 48px);
 }
 @media (min-resolution: 2dppx) {
   :root[devtoolstheme="dark"] .findbar-closebutton:not(:hover),
   .tab-close-button[visuallyselected=true]:not(:hover) {
     -moz-image-region: rect(0, 128px, 32px, 96px);
   }
 }
+
+.ac-type-icon {
+  /* Left-align the type icon in awesomebar popup results with the icon in the
+     urlbar. */
+  -moz-margin-start: 14px;
+}
--- a/browser/themes/osx/syncedtabs/sidebar.css
+++ b/browser/themes/osx/syncedtabs/sidebar.css
@@ -125,16 +125,18 @@ html {
   -moz-user-select: text;
   text-shadow: none;
 }
 
 .search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
   background-image: url(chrome://global/skin/icons/searchfield-cancel.svg);
   background-repeat: no-repeat;
   background-size: 11px 11px;
+  width: 11px;
+  height: 11px;
 }
 
 .search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-icon {
   display: none;
 }
 
 .search-box[focused="true"] {
   -moz-border-top-colors: -moz-mac-focusring -moz-mac-focusring #000000;
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -162,12 +162,14 @@
   skin/classic/browser/privatebrowsing/check.png               (../shared/privatebrowsing/check.png)
   skin/classic/browser/privatebrowsing/check@2x.png            (../shared/privatebrowsing/check@2x.png)
   skin/classic/browser/privatebrowsing/mask.svg                (../shared/privatebrowsing/mask.svg)
   skin/classic/browser/privatebrowsing/shield-page.png         (../shared/privatebrowsing/shield-page.png)
   skin/classic/browser/privatebrowsing/shield-page@2x.png      (../shared/privatebrowsing/shield-page@2x.png)
   skin/classic/browser/devedition/urlbar-history-dropmarker.svg (../shared/devedition/urlbar-history-dropmarker.svg)
   skin/classic/browser/devedition/urlbar-arrow.png             (../shared/devedition/urlbar-arrow.png)
   skin/classic/browser/devedition/urlbar-arrow@2x.png          (../shared/devedition/urlbar-arrow@2x.png)
+  skin/classic/browser/urlbar-star.svg                         (../shared/urlbar-star.svg)
+  skin/classic/browser/urlbar-tab.svg                          (../shared/urlbar-tab.svg)
   skin/classic/browser/usercontext/personal.svg                (../shared/usercontext/personal.svg)
   skin/classic/browser/usercontext/work.svg                    (../shared/usercontext/work.svg)
   skin/classic/browser/usercontext/banking.svg                 (../shared/usercontext/banking.svg)
   skin/classic/browser/usercontext/shopping.svg                (../shared/usercontext/shopping.svg)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/urlbar-star.svg
@@ -0,0 +1,20 @@
+<?xml version="1.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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+  <style>
+    path:not(:target) {
+      display: none;
+    }
+    path {
+      fill: #b2b2b2;
+    }
+    path[id$="-inverted"] {
+      fill: #fff;
+    }
+  </style>
+
+	<path id="star" d="M8.7,0.5l2,4.3l4.6,0.7c0.6,0.1,0.9,0.9,0.4,1.4l-3.3,3.4l0.8,4.8c0.1,0.7-0.6,1.2-1.1,0.9L8,13.7l-4.1,2.3 c-0.6,0.3-1.2-0.2-1.1-0.9l0.8-4.8L0.2,6.9C-0.2,6.4,0,5.6,0.7,5.5l4.6-0.7l2-4.3C7.6-0.1,8.4-0.1,8.7,0.5z"/>
+	<path id="star-inverted" d="M8.7,0.5l2,4.3l4.6,0.7c0.6,0.1,0.9,0.9,0.4,1.4l-3.3,3.4l0.8,4.8c0.1,0.7-0.6,1.2-1.1,0.9L8,13.7l-4.1,2.3 c-0.6,0.3-1.2-0.2-1.1-0.9l0.8-4.8L0.2,6.9C-0.2,6.4,0,5.6,0.7,5.5l4.6-0.7l2-4.3C7.6-0.1,8.4-0.1,8.7,0.5z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/urlbar-tab.svg
@@ -0,0 +1,21 @@
+<?xml version="1.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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+  <style>
+    path:not(:target) {
+      display: none;
+    }
+    path {
+      fill: #b2b2b2;
+    }
+    path[id$="-inverted"] {
+      fill: #fff;
+    }
+  </style>
+
+  <path id="tab" d="M14,9.5V6c0-1.7-1.3-3-3-3H5C3.3,3,2,4.3,2,6v3.5C2,10.3,1.3,11,0.5,11h0C0.2,11,0,11.2,0,11.5v1 C0,12.8,0.2,13,0.5,13h15c0.3,0,0.5-0.2,0.5-0.5v-1c0-0.3-0.2-0.5-0.5-0.5h0C14.7,11,14,10.3,14,9.5z"/>
+  <path id="tab-inverted" d="M14,9.5V6c0-1.7-1.3-3-3-3H5C3.3,3,2,4.3,2,6v3.5C2,10.3,1.3,11,0.5,11h0C0.2,11,0,11.2,0,11.5v1 C0,12.8,0.2,13,0.5,13h15c0.3,0,0.5-0.2,0.5-0.5v-1c0-0.3-0.2-0.5-0.5-0.5h0C14.7,11,14,10.3,14,9.5z"/>
+
+</svg>
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1133,21 +1133,16 @@ toolbar[brighttext] .toolbarbutton-1 > .
   border-color: var(--urlbar-border-color);
 }
 
 #urlbar:hover,
 .searchbar-textbox:hover {
   border-color: var(--urlbar-border-color-hover);
 }
 
-/* overlap the urlbar's border */
-#PopupAutoCompleteRichResult {
-  margin-top: -1px;
-}
-
 @media (-moz-windows-default-theme) {
   #urlbar,
   .searchbar-textbox {
     border-radius: 1px;
   }
 
   @media (-moz-os-version: windows-vista),
          (-moz-os-version: windows-win7),
@@ -1408,135 +1403,189 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 %include ../shared/identity-block/identity-block.inc.css
 
 /* autocomplete */
 
 #treecolAutoCompleteImage {
   max-width: 36px;
 }
 
-.ac-result-type-bookmark,
+.autocomplete-richlistbox {
+  padding: 4px;
+}
+
+.autocomplete-richlistitem {
+  height: 30px;
+  min-height: 30px;
+  font: message-box;
+  border-radius: 2px;
+  border: 1px solid transparent;
+}
+
+.autocomplete-richlistitem[selected=true] {
+  background-color: hsl(210, 80%, 52%);
+}
+
+.ac-title {
+  font-size: 14px;
+  color: hsl(0, 0%, 0%);
+}
+
+html|span.ac-emphasize-text-title {
+  color: hsl(0, 0%, 50%);
+}
+
+.ac-tags {
+  font-size: 12px;
+}
+
+html|span.ac-tag {
+  background-color: hsl(216, 0%, 88%);
+  color: hsl(0, 0%, 0%);
+  border-radius: 2px;
+  border: 1px solid transparent;
+  padding: 0 1px;
+}
+
+html|span.ac-emphasize-text-tag {
+  color: hsl(0, 0%, 50%);
+}
+
+.ac-url {
+  font-size: 12px;
+  color: hsl(210, 77%, 47%);
+}
+
+html|span.ac-emphasize-text-url {
+  color: hsl(210, 86%, 64%);
+}
+
+.ac-action {
+  font-size: 12px;
+  color: hsl(178, 100%, 28%);
+}
+
+html|span.ac-title-urlaction-separator {
+  color: hsl(0, 0%, 50%);
+}
+
+.ac-title[selected=true],
+.ac-url[selected=true],
+.ac-action[selected=true],
+.ac-title-text[selected=true] > html|span.ac-emphasize-text,
+.ac-url-text[selected=true] > html|span.ac-emphasize-text,
+.ac-action-text[selected=true] > html|span.ac-emphasize-text,
+.ac-url-text[selected=true] > html|span.ac-title-urlaction-separator,
+.ac-action-text[selected=true] > html|span.ac-title-urlaction-separator {
+  color: hsl(0, 0%, 100%);
+}
+
+.ac-tags-text[selected] > html|span.ac-tag {
+  background-color: hsl(0, 0%, 100%);
+  color: hsl(210, 80%, 40%);
+}
+
+.ac-tags-text[selected] > html|span.ac-tag > html|span.ac-emphasize-text-tag {
+  color: hsl(210, 80%, 52%);
+}
+
+@media not all and (-moz-windows-default-theme) {
+  .autocomplete-richlistitem[selected=true] {
+    background-color: Highlight;
+  }
+
+  .ac-title,
+  html|span.ac-emphasize-text-title {
+    color: inherit;
+  }
+
+  html|span.ac-tag,
+  html|span.ac-emphasize-text-tag {
+    background-color: -moz-FieldText;
+    color: -moz-Field;
+  }
+
+  .ac-url,
+  .ac-action,
+  html|span.ac-emphasize-text-url,
+  html|span.ac-title-urlaction-separator {
+    color: -moz-nativehyperlinktext;
+  }
+
+  .ac-tags-text[selected] > html|span.ac-tag,
+  .ac-tags-text[selected] > html|span.ac-tag > html|span.ac-emphasize-text-tag {
+    background-color: HighlightText;
+    color: Highlight;
+  }
+}
+
+.ac-type-icon[type=bookmark] {
+  list-style-image: url("chrome://browser/skin/urlbar-star.svg#star");
+}
+
+.ac-type-icon[type=bookmark][selected][current] {
+  list-style-image: url("chrome://browser/skin/urlbar-star.svg#star-inverted");
+}
+
 .autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/autocomplete-star.png");
   -moz-image-region: rect(0 16px 16px 0);
   width: 16px;
   height: 16px;
 }
 
 @media (min-resolution: 1.1dppx) {
-  .ac-result-type-bookmark,
   .autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
     list-style-image: url("chrome://browser/skin/places/autocomplete-star@2x.png");
     -moz-image-region: rect(0 32px 32px 0);
   }
 }
 
-.ac-result-type-keyword,
-.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage),
-richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-site-icon {
-  list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
-  width: 16px;
-  height: 16px;
-}
-
 @media not all and (-moz-os-version: windows-vista) and (-moz-windows-default-theme) {
   @media not all and (-moz-os-version: windows-win7) and (-moz-windows-default-theme) {
-    richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark,
     .autocomplete-treebody::-moz-tree-image(selected, current, bookmark, treecolAutoCompleteImage) {
       -moz-image-region: rect(0 32px 16px 16px);
     }
 
     @media (min-resolution: 1.1dppx) {
-      richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark,
       .autocomplete-treebody::-moz-tree-image(selected, current, bookmark, treecolAutoCompleteImage) {
         -moz-image-region: rect(0 64px 32px 32px);
       }
     }
 
-    .ac-result-type-keyword[selected="true"],
-    .autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected),
-    richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-title-box > .ac-site-icon {
+    .autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected) {
       list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon-inverted);
     }
   }
 }
 
-.ac-result-type-tag,
+.ac-type-icon[type=keyword],
+.ac-site-icon[type=searchengine],
+.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage) {
+  list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
+}
+
+.ac-type-icon[type=keyword][selected],
+.ac-site-icon[type=searchengine][selected],
+.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected) {
+  list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon-inverted);
+}
+
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-comment,
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description,
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
-  font-size: 1.06em;
-}
-
-.ac-extra > .ac-comment,
-.ac-url-text,
-.ac-action-text {
-  font-size: 1em;
-}
-
-.ac-url-text,
-.ac-action-text {
-  color: -moz-nativehyperlinktext;
-}
-
-@media (-moz-os-version: windows-xp) and (-moz-windows-default-theme) {
-  .ac-url-text:not([selected="true"]),
-  .ac-action-text:not([selected="true"]) {
-    color: #008800;
-  }
-}
-
-@media (-moz-os-version: windows-win10) and (-moz-windows-default-theme) {
-  .ac-url-text:not([selected="true"]),
-  .ac-action-text:not([selected="true"]) {
-    color: Highlight;
-  }
-}
-
-richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
-  list-style-image: url("chrome://browser/skin/actionicon-tab.png");
-  -moz-image-region: rect(0, 16px, 11px, 0);
-  padding: 0 3px;
-  width: 22px;
-  height: 11px;
-}
-
-@media (min-resolution: 1.1dppx) {
-  richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon {
-    list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
-    -moz-image-region: rect(0, 32px, 22px, 0);
-  }
-}
-
-@media not all and (-moz-os-version: windows-vista),
-       not all and (-moz-windows-default-theme) {
-  @media not all and (-moz-os-version: windows-win7),
-         not all and (-moz-windows-default-theme) {
-    richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-      -moz-image-region: rect(11px, 16px, 22px, 0);
-    }
-
-    @media (min-resolution: 1.1dppx) {
-      richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon {
-        -moz-image-region: rect(22px, 32px, 44px, 0);
-      }
-    }
-
-    .ac-comment[selected="true"],
-    .ac-url-text[selected="true"],
-    .ac-action-text[selected="true"] {
-      color: inherit !important;
-    }
-  }
+.ac-type-icon[type=switchtab] {
+  list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab");
+}
+
+.ac-type-icon[type=switchtab][selected] {
+  list-style-image: url("chrome://browser/skin/urlbar-tab.svg#tab-inverted");
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .autocomplete-treebody::-moz-tree-cell-text(suggesthint, treecolAutoCompleteComment),
 .autocomplete-treebody::-moz-tree-cell-text(suggestfirst, treecolAutoCompleteComment)
--- a/browser/themes/windows/devedition.css
+++ b/browser/themes/windows/devedition.css
@@ -319,8 +319,14 @@
   :root[devtoolstheme="light"] #titlebar-close {
     list-style-image: url(chrome://browser/skin/caption-buttons.svg#close);
   }
 
   :root[devtoolstheme="light"] #titlebar-close:hover {
     list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
   }
 }
+
+.ac-type-icon {
+  /* Left-align the type icon in awesomebar popup results with the icon in the
+     urlbar. */
+  -moz-margin-start: 13px;
+}
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -16,8 +16,17 @@
 responsive.title=Responsive Design Mode
 
 # LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
 responsive.exit=Close Responsive Design Mode
 
 # LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
 # device selector
 responsive.noDeviceSelected=no device selected
+
+# LOCALIZATION NOTE  (responsive.screenshot): tooltip of the screenshot button.
+responsive.screenshot=Take a screenshot of the viewport
+
+# LOCALIZATION NOTE (responsive.screenshotGeneratedFilename): The auto generated
+# filename.
+# The first argument (%1$S) is the date string in yyyy-mm-dd format and the
+# second argument (%2$S) is the time string in HH.MM.SS format.
+responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -277,20 +277,42 @@ handlers[actions.FETCH_DOMINATOR_TREE_EN
     if (snapshot.id !== id) {
       return snapshot;
     }
 
     assert(snapshot.dominatorTree, "Should have a dominator tree model");
     assert(snapshot.dominatorTree.state == dominatorTreeState.FETCHING,
            "Should be in the FETCHING state");
 
+    let focused;
+    if (snapshot.dominatorTree.focused) {
+      focused = (function findFocused(node) {
+        if (node.nodeId === snapshot.dominatorTree.focused.nodeId) {
+          return node;
+        }
+
+        if (node.children) {
+          const length = node.children.length;
+          for (let i = 0; i < length; i++) {
+            const result = findFocused(node.children[i]);
+            if (result) {
+              return result;
+            }
+          }
+        }
+
+        return undefined;
+      }(root));
+    }
+
     const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
       state: dominatorTreeState.LOADED,
       root,
       expanded: Immutable.Set(),
+      focused,
     });
 
     return immutableUpdate(snapshot, { dominatorTree });
   });
 };
 
 handlers[actions.EXPAND_DOMINATOR_TREE_NODE] = function (snapshots, { id, node }) {
   return snapshots.map(snapshot => {
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_dominator_trees_10.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we maintain focus of the selected dominator tree node across
+// changing breakdowns for labeling them.
+
+let {
+  snapshotState: states,
+  dominatorTreeState,
+  dominatorTreeDisplays,
+  viewState,
+} = require("devtools/client/memory/constants");
+let {
+  takeSnapshotAndCensus,
+  focusDominatorTreeNode,
+} = require("devtools/client/memory/actions/snapshot");
+const {
+  changeView,
+} = require("devtools/client/memory/actions/view");
+const {
+  setDominatorTreeDisplayAndRefresh,
+} = require("devtools/client/memory/actions/dominator-tree-display");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  dispatch(changeView(viewState.DOMINATOR_TREE));
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+
+  // Wait for the dominator tree to finish being fetched.
+  yield waitUntilState(store, state =>
+    state.snapshots[0] &&
+    state.snapshots[0].dominatorTree &&
+    state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+  ok(true, "The dominator tree was fetched");
+
+  const root = getState().snapshots[0].dominatorTree.root;
+  ok(root, "When the dominator tree is loaded, we should have its root");
+
+  dispatch(focusDominatorTreeNode(getState().snapshots[0].id, root));
+  equal(root, getState().snapshots[0].dominatorTree.focused,
+        "The root should be focused.");
+
+  equal(getState().dominatorTreeDisplay, dominatorTreeDisplays.coarseType,
+        "Using dominatorTreeDisplays.coarseType by default");
+  dispatch(setDominatorTreeDisplayAndRefresh(heapWorker,
+                                             dominatorTreeDisplays.allocationStack));
+  equal(getState().dominatorTreeDisplay, dominatorTreeDisplays.allocationStack,
+        "Using dominatorTreeDisplays.allocationStack now");
+
+  yield waitUntilState(store, state =>
+    state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING);
+  ok(true, "We started re-fetching the dominator tree");
+
+  yield waitUntilState(store, state =>
+    state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED);
+  ok(true, "The dominator tree was loaded again");
+
+  ok(getState().snapshots[0].dominatorTree.focused,
+     "Still have a focused node");
+  equal(getState().snapshots[0].dominatorTree.focused.nodeId, root.nodeId,
+        "Focused node is the same as before");
+
+  heapWorker.destroy();
+  yield front.detach();
+});
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -37,12 +37,13 @@ skip-if = toolkit == 'android' || toolki
 [test_dominator_trees_02.js]
 [test_dominator_trees_03.js]
 [test_dominator_trees_04.js]
 [test_dominator_trees_05.js]
 [test_dominator_trees_06.js]
 [test_dominator_trees_07.js]
 [test_dominator_trees_08.js]
 [test_dominator_trees_09.js]
+[test_dominator_trees_10.js]
 [test_tree-map-01.js]
 [test_tree-map-02.js]
 [test_utils.js]
 [test_utils-get-snapshot-totals.js]
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -27,16 +27,22 @@ createEnum([
   "CHANGE_LOCATION",
 
   // Resize the viewport.
   "RESIZE_VIEWPORT",
 
   // Rotate the viewport.
   "ROTATE_VIEWPORT",
 
+  // Take a screenshot of the viewport.
+  "TAKE_SCREENSHOT_START",
+
+  // Indicates when the screenshot action ends.
+  "TAKE_SCREENSHOT_END",
+
 ], module.exports);
 
 /**
  * Create a simple enum-like object with keys mirrored to values from an array.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
 function createEnum(array, target) {
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -3,10 +3,11 @@
 # 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/.
 
 DevToolsModules(
     'devices.js',
     'index.js',
     'location.js',
+    'screenshot.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const {
+  TAKE_SCREENSHOT_START,
+  TAKE_SCREENSHOT_END,
+} = require("./index");
+
+const { getRect } = require("devtools/shared/layout/utils");
+const { getFormatStr } = require("../utils/l10n");
+const { getToplevelWindow } = require("sdk/window/utils");
+const { Task: { spawn, async } } = require("resource://gre/modules/Task.jsm");
+
+const BASE_URL = "resource://devtools/client/responsive.html";
+const audioCamera = new window.Audio(`${BASE_URL}/audio/camera-click.mp3`);
+
+function getFileName() {
+  let date = new Date();
+  let month = ("0" + (date.getMonth() + 1)).substr(-2);
+  let day = ("0" + date.getDate()).substr(-2);
+  let dateString = [date.getFullYear(), month, day].join("-");
+  let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+
+  return getFormatStr("responsive.screenshotGeneratedFilename", dateString,
+                      timeString);
+}
+
+function createScreenshotFor(node) {
+  let { top, left, width, height } = getRect(window, node, window);
+
+  const canvas = document.createElementNS(HTML_NS, "canvas");
+  const ctx = canvas.getContext("2d");
+  const ratio = window.devicePixelRatio;
+  canvas.width = width * ratio;
+  canvas.height = height * ratio;
+  ctx.scale(ratio, ratio);
+  ctx.drawWindow(window, left, top, width, height, "#fff");
+
+  return canvas.toDataURL("image/png", "");
+}
+
+function saveToFile(data, filename) {
+  return spawn(function* () {
+    const chromeWindow = getToplevelWindow(window);
+    const chromeDocument = chromeWindow.document;
+
+    // append .png extension to filename if it doesn't exist
+    filename = filename.replace(/\.png$|$/i, ".png");
+
+    chromeWindow.saveURL(data, filename, null,
+                         true, true,
+                         chromeDocument.documentURIObject, chromeDocument);
+  });
+}
+
+function simulateCameraEffects(node) {
+  audioCamera.play();
+  node.animate({ opacity: [ 0, 1 ] }, 500);
+}
+
+module.exports = {
+
+  takeScreenshot() {
+    return function* (dispatch, getState) {
+      yield dispatch({ type: TAKE_SCREENSHOT_START });
+
+      // Waiting the next repaint, to ensure the react components
+      // can be properly render after the action dispatched above
+      window.requestAnimationFrame(async(function* () {
+        let iframe = document.querySelector("iframe");
+        let data = createScreenshotFor(iframe);
+
+        simulateCameraEffects(iframe);
+
+        yield saveToFile(data, getFileName());
+
+        dispatch({ type: TAKE_SCREENSHOT_END });
+      }));
+    };
+  }
+
+};
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -1,74 +1,90 @@
 /* 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/. */
 
+ /* eslint-env browser */
+
 "use strict";
 
 const { createClass, createFactory, PropTypes, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
   changeDevice,
   resizeViewport,
   rotateViewport
 } = require("./actions/viewports");
+const { takeScreenshot } = require("./actions/screenshot");
 const Types = require("./types");
 const Viewports = createFactory(require("./components/viewports"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 
 let App = createClass({
 
   displayName: "App",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
-    onExit: PropTypes.func.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
   onChangeViewportDevice(id, device) {
     this.props.dispatch(changeDevice(id, device));
   },
 
+  onExit() {
+    window.postMessage({ type: "exit" }, "*");
+  },
+
   onResizeViewport(id, width, height) {
     this.props.dispatch(resizeViewport(id, width, height));
   },
 
   onRotateViewport(id) {
     this.props.dispatch(rotateViewport(id));
   },
 
+  onScreenshot() {
+    this.props.dispatch(takeScreenshot());
+  },
+
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewports,
-      onExit,
     } = this.props;
 
     let {
       onChangeViewportDevice,
+      onExit,
       onResizeViewport,
       onRotateViewport,
+      onScreenshot,
     } = this;
 
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
+        screenshot,
         onExit,
+        onScreenshot,
       }),
       Viewports({
         devices,
         location,
+        screenshot,
         viewports,
         onChangeViewportDevice,
         onRotateViewport,
         onResizeViewport,
       })
     );
   },
 
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6d9af013315d873e910ecf6e15bc298093dc1e99
GIT binary patch
literal 27634
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
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/audio/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+    'camera-click.mp3',
+)
--- a/devtools/client/responsive.html/components/device-selector.js
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { getStr } = require("./utils/l10n");
+const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "DeviceSelector",
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -1,44 +1,56 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { getStr } = require("./utils/l10n");
+const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
+const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "GlobalToolbar",
 
-  mixins: [ addons.PureRenderMixin ],
-
   propTypes: {
     onExit: PropTypes.func.isRequired,
+    onScreenshot: PropTypes.func.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
+  mixins: [ addons.PureRenderMixin ],
+
   render() {
     let {
       onExit,
+      onScreenshot,
+      screenshot,
     } = this.props;
 
     return dom.header(
       {
         id: "global-toolbar",
         className: "toolbar",
       },
       dom.span(
         {
           className: "title",
         },
         getStr("responsive.title")),
       dom.button({
+        id: "global-screenshot-button",
+        className: "toolbar-button devtools-button",
+        title: getStr("responsive.screenshot"),
+        onClick: onScreenshot,
+        disabled: screenshot.isCapturing,
+      }),
+      dom.button({
         id: "global-exit-button",
         className: "toolbar-button devtools-button",
         title: getStr("responsive.exit"),
         onClick: onExit,
       })
     );
   },
 
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,18 +1,14 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-DIRS += [
-    'utils',
-]
-
 DevToolsModules(
     'browser.js',
     'device-selector.js',
     'global-toolbar.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -19,16 +19,17 @@ const VIEWPORT_MIN_HEIGHT = Constants.MI
 
 module.exports = createClass({
 
   displayName: "ResizableViewport",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     return {
@@ -107,22 +108,29 @@ module.exports = createClass({
       lastClientY
     });
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewport,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
 
+    let resizeHandleClass = "viewport-resize-handle";
+
+    if (screenshot.isCapturing) {
+      resizeHandleClass += " hidden";
+    }
+
     return dom.div(
       {
         className: "resizable-viewport",
       },
       ViewportToolbar({
         devices,
         selectedDevice: viewport.device,
         onChangeViewportDevice,
@@ -131,17 +139,17 @@ module.exports = createClass({
       }),
       Browser({
         location,
         width: viewport.width,
         height: viewport.height,
         isResizing: this.state.isResizing
       }),
       dom.div({
-        className: "viewport-resize-handle",
+        className: resizeHandleClass,
         onMouseDown: this.onResizeStart,
       }),
       dom.div({
         ref: "resizeBarX",
         className: "viewport-horizontal-resize-handle",
         onMouseDown: this.onResizeStart,
       }),
       dom.div({
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -13,16 +13,17 @@ const ViewportDimension = createFactory(
 
 module.exports = createClass({
 
   displayName: "Viewport",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   onChangeViewportDevice(device) {
     let {
@@ -50,32 +51,34 @@ module.exports = createClass({
 
     onRotateViewport(viewport.id);
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewport,
     } = this.props;
 
     let {
       onChangeViewportDevice,
       onRotateViewport,
       onResizeViewport,
     } = this;
 
     return dom.div(
       {
         className: "viewport",
       },
       ResizableViewport({
         devices,
         location,
+        screenshot,
         viewport,
         onChangeViewportDevice,
         onResizeViewport,
         onRotateViewport,
       }),
       ViewportDimension({
         viewport,
         onChangeViewportDevice,
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -12,41 +12,44 @@ const Viewport = createFactory(require("
 
 module.exports = createClass({
 
   displayName: "Viewports",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewports,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
 
     return dom.div(
       {
         id: "viewports",
       },
       viewports.map(viewport => {
         return Viewport({
           key: viewport.id,
           devices,
           location,
+          screenshot,
           viewport,
           onChangeViewportDevice,
           onResizeViewport,
           onRotateViewport,
         });
       })
     );
   },
--- a/devtools/client/responsive.html/images/moz.build
+++ b/devtools/client/responsive.html/images/moz.build
@@ -3,10 +3,11 @@
 # 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/.
 
 DevToolsModules(
     'close.svg',
     'grippers.svg',
     'rotate-viewport.svg',
+    'screenshot.svg',
     'select-arrow.svg',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/images/screenshot.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#babec3">
+  <path d="M15.5 14H.5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5H4V2.5c0-.3.2-.5.5-.5h7c.3 0 .5.2.5.5V5h3.5c.3 0 .5.2.5.5v8c0 .3-.2.5-.5.5zM1 13h14V6h-3.5c-.3 0-.5-.2-.5-.5V3H5v2.5c0 .3-.2.5-.5.5H1v7z"/>
+  <path d="M8 12c-1.6 0-2.9-1.3-2.9-2.9S6.4 6.2 8 6.2c1.6 0 2.9 1.3 2.9 2.9S9.6 12 8 12zm0-4.8c-1.1 0-1.9.8-1.9 1.9 0 1.1.8 1.9 1.9 1.9 1.1 0 1.9-.9 1.9-1.9C9.9 8 9.1 7.2 8 7.2z"/>
+</svg>
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -86,27 +86,38 @@ body {
   border-right: 1px solid var(--theme-splitter-color);
   padding: 1px 6px 0 2px;
 }
 
 #global-toolbar .toolbar-button {
   margin: 0 0 0 5px;
 }
 
-#global-exit-button,
-#global-exit-button::before {
+#global-toolbar .toolbar-button,
+#global-toolbar .toolbar-button::before {
   width: 12px;
   height: 12px;
 }
 
+#global-screenshot-button::before {
+  background-image: url("./images/screenshot.svg");
+  margin: -6px 0 0 -6px;
+}
+
 #global-exit-button::before {
   background-image: url("./images/close.svg");
   margin: -6px 0 0 -6px;
 }
 
+#global-screenshot-button:disabled {
+  filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
+  opacity: 1 !important;
+}
+
+
 #viewports {
   /* Snap to the top of the app when there isn't enough vertical space anymore
      to center the viewports (so we don't loose the toolbar) */
   position: sticky;
   top: 0;
   /* Make sure left-most viewport is visible when there's horizontal overflow.
      That is, when the horizontal space become smaller than the viewports and a
      scrollbar appears, then the first viewport will still be visible */
@@ -215,16 +226,20 @@ body {
   background-image: url("./images/grippers.svg");
   background-position: bottom right;
   padding: 0 1px 1px 0;
   background-repeat: no-repeat;
   background-origin: content-box;
   cursor: se-resize;
 }
 
+.viewport-resize-handle.hidden {
+  display: none;
+}
+
 .viewport-horizontal-resize-handle {
   position: absolute;
   width: 5px;
   height: calc(100% - 16px);
   right: -4px;
   top: 0;
   cursor: e-resize;
 }
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -37,20 +37,18 @@ let bootstrap = {
   init() {
     // Load a special UA stylesheet to reset certain styles such as dropdown
     // lists.
     loadSheet(window,
               "resource://devtools/client/responsive.html/responsive-ua.css",
               "agent");
     this.telemetry.toolOpened("responsive");
     let store = this.store = Store();
-    let app = App({
-      onExit: () => window.postMessage({ type: "exit" }, "*"),
-    });
-    let provider = createElement(Provider, { store }, app);
+    let provider = createElement(Provider, { store }, App());
+
     ReactDOM.render(provider, document.querySelector("#root"));
     this.initDevices();
     window.postMessage({ type: "init" }, "*");
   },
 
   destroy() {
     this.store = null;
     this.telemetry.toolClosed("responsive");
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -1,19 +1,21 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DIRS += [
     'actions',
+    'audio',
     'components',
     'images',
     'reducers',
+    'utils',
 ]
 
 DevToolsModules(
     'app.js',
     'constants.js',
     'index.css',
     'manager.js',
     'reducers.js',
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -1,9 +1,10 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 exports.devices = require("./reducers/devices");
 exports.location = require("./reducers/location");
+exports.screenshot = require("./reducers/screenshot");
 exports.viewports = require("./reducers/viewports");
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -2,10 +2,11 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'devices.js',
     'location.js',
+    'screenshot.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/screenshot.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  TAKE_SCREENSHOT_END,
+  TAKE_SCREENSHOT_START,
+} = require("../actions/index");
+
+const INITIAL_SCREENSHOT = { isCapturing: false };
+
+let reducers = {
+
+  [TAKE_SCREENSHOT_END](screenshot, action) {
+    return Object.assign({}, screenshot, { isCapturing: false });
+  },
+
+  [TAKE_SCREENSHOT_START](screenshot, action) {
+    return Object.assign({}, screenshot, { isCapturing: true });
+  },
+};
+
+module.exports = function(screenshot = INITIAL_SCREENSHOT, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return screenshot;
+  }
+  return reducer(screenshot, action);
+};
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -3,9 +3,10 @@ tags = devtools
 subsuite = devtools
 support-files =
   devices.json
   head.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
 
 [browser_exit_button.js]
+[browser_screenshot_button.js]
 [browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global exit button
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+
+function* waitUntilScreenshot() {
+  return new Promise(Task.async(function* (resolve) {
+    let { Downloads } = require("resource://gre/modules/Downloads.jsm");
+    let list = yield Downloads.getList(Downloads.ALL);
+
+    let view = {
+      onDownloadAdded: download => {
+        download.whenSucceeded().then(() => {
+          resolve(download.target.path);
+          list.removeView(view);
+        });
+      }
+    };
+
+    yield list.addView(view);
+  }));
+}
+
+addRDMTask(TEST_URL, function* ({ ui: {toolWindow} }) {
+  let { store, document } = toolWindow;
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  info("Click the screenshot button");
+  let screenshotButton = document.getElementById("global-screenshot-button");
+  screenshotButton.click();
+
+  let whenScreenshotSucceeded = waitUntilScreenshot();
+
+  let filePath = yield whenScreenshotSucceeded;
+  let image = new Image();
+  image.src = OS.Path.toFileURI(filePath);
+
+  yield once(image, "load");
+
+  // We have only one viewport at the moment
+  let viewport = store.getState().viewports[0];
+  let ratio = window.devicePixelRatio;
+
+  is(image.width, viewport.width * ratio,
+    "screenshot width has the expected width");
+
+  is(image.height, viewport.height * ratio,
+    "screenshot width has the expected height");
+
+  yield OS.File.remove(filePath);
+});
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -66,16 +66,25 @@ exports.devices = {
 };
 
 /**
  * The location of the document displayed in the viewport(s).
  */
 exports.location = PropTypes.string;
 
 /**
+ * The progression of the screenshot
+ */
+exports.screenshot = {
+
+  isCapturing: PropTypes.bool.isRequired,
+
+};
+
+/**
  * A single viewport displaying a document.
  */
 exports.viewport = {
 
   // The id of the viewport
   id: PropTypes.number.isRequired,
 
   // The currently selected device applied to the viewport.
rename from devtools/client/responsive.html/components/utils/l10n.js
rename to devtools/client/responsive.html/utils/l10n.js
rename from devtools/client/responsive.html/components/utils/moz.build
rename to devtools/client/responsive.html/utils/moz.build
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -9,8 +9,9 @@ support-files =
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
 [test_tree_07.html]
 [test_tree_08.html]
 [test_tree_09.html]
 [test_tree_10.html]
+[test_tree_11.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_11.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is focused by arrow key, the view is scrolled.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Tree component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+  <style>
+   * {
+       margin: 0;
+       padding: 0;
+       height: 30px;
+       max-height: 30px;
+       min-height: 30px;
+       font-size: 10px;
+       overflow: auto;
+   }
+  </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+  try {
+    const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+    const React = browserRequire("devtools/client/shared/vendor/react");
+    const { Simulate } = React.addons.TestUtils;
+    const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+    TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+    const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+    yield setProps(tree, {
+      itemHeight: 10,
+      onFocus: item => setProps(tree, { focused: item }),
+      focused: "K",
+    });
+    yield setState(tree, {
+      scroll: 10,
+    });
+    yield forceRender(tree);
+
+    isRenderedTree(document.body.textContent, [
+      "A:false",
+      "-B:false",
+      "--E:false",
+      "---K:true",
+      "---L:false",
+    ], "Should render initial correctly");
+
+    yield new Promise(resolve => {
+      const treeElem = document.querySelector(".tree");
+      treeElem.addEventListener("scroll", function onScroll() {
+        dumpn("Got scroll event");
+        treeElem.removeEventListener("scroll", onScroll);
+        resolve();
+      });
+
+      dumpn("Sending ArrowDown key");
+      Simulate.keyDown(treeElem, { key: "ArrowDown" });
+    });
+
+    dumpn("Forcing re-render");
+    yield forceRender(tree);
+
+    isRenderedTree(document.body.textContent, [
+      "-B:false",
+      "--E:false",
+      "---K:false",
+      "---L:true",
+      "--F:false",
+    ], "Should have scrolled down one");
+
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/shared/components/tree.js
+++ b/devtools/client/shared/components/tree.js
@@ -56,17 +56,17 @@ const TreeNode = createFactory(createCla
   },
 
   render() {
     const arrow = ArrowExpander({
       item: this.props.item,
       expanded: this.props.expanded,
       visible: this.props.hasChildren,
       onExpand: this.props.onExpand,
-      onCollapse: this.props.onCollapse
+      onCollapse: this.props.onCollapse,
     });
 
     let isOddRow = this.props.index % 2;
     return dom.div(
       {
         className: `tree-node div ${isOddRow ? "tree-node-odd" : ""}`,
         onFocus: this.props.onFocus,
         onClick: this.props.onFocus,
@@ -110,24 +110,27 @@ const TreeNode = createFactory(createCla
  * Create a function that calls the given function `fn` only once per animation
  * frame.
  *
  * @param {Function} fn
  * @returns {Function}
  */
 function oncePerAnimationFrame(fn) {
   let animationId = null;
+  let argsToPass = null;
   return function (...args) {
+    argsToPass = args;
     if (animationId !== null) {
       return;
     }
 
     animationId = requestAnimationFrame(() => {
+      fn.call(this, ...argsToPass);
       animationId = null;
-      fn.call(this, ...args);
+      argsToPass = null;
     });
   };
 }
 
 const NUMBER_OF_OFFSCREEN_ITEMS = 1;
 
 /**
  * A generic tree component. See propTypes for the public API.
@@ -230,19 +233,21 @@ const Tree = module.exports = createClas
     for (let i = 0; i < length; i++) {
       autoExpand(roots[i], 0);
     }
   },
 
   render() {
     const traversal = this._dfsFromRoots();
 
-    // Remove 1 from `begin` and add 2 to `end` so that the top and bottom of
-    // the page are filled with the previous and next items respectively,
-    // rather than whitespace if the item is not in full view.
+    // Remove `NUMBER_OF_OFFSCREEN_ITEMS` from `begin` and add `2 *
+    // NUMBER_OF_OFFSCREEN_ITEMS` to `end` so that the top and bottom of the
+    // page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS` previous and next
+    // items respectively, rather than whitespace if the item is not in full
+    // view.
     const begin = Math.max(((this.state.scroll / this.props.itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS, 0);
     const end = begin + (2 * NUMBER_OF_OFFSCREEN_ITEMS) + ((this.state.height / this.props.itemHeight) | 0);
     const toRender = traversal.slice(begin, end);
 
     const nodes = [
       dom.div({
         key: "top-spacer",
         style: {
@@ -261,17 +266,17 @@ const Tree = module.exports = createClas
         item: item,
         depth: depth,
         renderItem: this.props.renderItem,
         focused: this.props.focused === item,
         expanded: this.props.isExpanded(item),
         hasChildren: !!this.props.getChildren(item).length,
         onExpand: this._onExpand,
         onCollapse: this._onCollapse,
-        onFocus: () => this._focus(item)
+        onFocus: () => this._focus(begin + i, item),
       }));
     }
 
     nodes.push(dom.div({
       key: "bottom-spacer",
       style: {
         padding: 0,
         margin: 0,
@@ -279,26 +284,47 @@ const Tree = module.exports = createClas
       }
     }));
 
     return dom.div(
       {
         className: "tree",
         ref: "tree",
         onKeyDown: this._onKeyDown,
+        onKeyPress: this._preventArrowKeyScrolling,
+        onKeyUp: this._preventArrowKeyScrolling,
         onScroll: this._onScroll,
         style: {
           padding: 0,
           margin: 0
         }
       },
       nodes
     );
   },
 
+  _preventArrowKeyScrolling(e) {
+    switch (e.key) {
+      case "ArrowUp":
+      case "ArrowDown":
+      case "ArrowLeft":
+      case "ArrowRight":
+        e.preventDefault();
+        e.stopPropagation();
+        if (e.nativeEvent) {
+          if (e.nativeEvent.preventDefault) {
+            e.nativeEvent.preventDefault();
+          }
+          if (e.nativeEvent.stopPropagation) {
+            e.nativeEvent.stopPropagation();
+          }
+        }
+    }
+  },
+
   /**
    * Updates the state's height based on clientHeight.
    */
   _updateHeight() {
     this.setState({
       height: this.refs.tree.clientHeight
     });
   },
@@ -372,29 +398,51 @@ const Tree = module.exports = createClas
     if (this.props.onCollapse) {
       this.props.onCollapse(item);
     }
   }),
 
   /**
    * Sets the passed in item to be the focused item.
    *
-   * @param {Object} item
+   * @param {Number} index
+   *        The index of the item in a full DFS traversal (ignoring collapsed
+   *        nodes). Ignored if `item` is undefined.
+   *
+   * @param {Object|undefined} item
+   *        The item to be focused, or undefined to focus no item.
    */
-  _focus(item) {
+  _focus(index, item) {
+    if (item !== undefined) {
+      const itemStartPosition = index * this.props.itemHeight;
+      const itemEndPosition = (index + 1) * this.props.itemHeight;
+
+      // Note that if the height of the viewport (this.state.height) is less than
+      // `this.props.itemHeight`, we could accidentally try and scroll both up and
+      // down in a futile attempt to make both the item's start and end positions
+      // visible. Instead, give priority to the start of the item by checking its
+      // position first, and then using an "else if", rather than a separate "if",
+      // for the end position.
+      if (this.state.scroll > itemStartPosition) {
+        this.refs.tree.scrollTo(0, itemStartPosition);
+      } else if ((this.state.scroll + this.state.height) < itemEndPosition) {
+        this.refs.tree.scrollTo(0, itemEndPosition - this.state.height);
+      }
+    }
+
     if (this.props.onFocus) {
       this.props.onFocus(item);
     }
   },
 
   /**
    * Sets the state to have no focused item.
    */
   _onBlur() {
-    this._focus(undefined);
+    this._focus(0, undefined);
   },
 
   /**
    * Fired on a scroll within the tree's container, updates
    * the stored position of the view port to handle virtual view rendering.
    *
    * @param {Event} e
    */
@@ -415,75 +463,73 @@ const Tree = module.exports = createClas
       return;
     }
 
     // Allow parent nodes to use navigation arrows with modifiers.
     if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
       return;
     }
 
-    // Prevent scrolling when pressing navigation keys. Guard against mocked
-    // events received when testing.
-    if (e.nativeEvent && e.nativeEvent.preventDefault) {
-      ViewHelpers.preventScrolling(e.nativeEvent);
-    }
+    this._preventArrowKeyScrolling(e);
 
     switch (e.key) {
       case "ArrowUp":
         this._focusPrevNode();
-        return false;
+        return;
 
       case "ArrowDown":
         this._focusNextNode();
-        return false;
+        return;
 
       case "ArrowLeft":
         if (this.props.isExpanded(this.props.focused)
             && this.props.getChildren(this.props.focused).length) {
           this._onCollapse(this.props.focused);
         } else {
           this._focusParentNode();
         }
-        return false;
+        return;
 
       case "ArrowRight":
         if (!this.props.isExpanded(this.props.focused)) {
           this._onExpand(this.props.focused);
         } else {
           this._focusNextNode();
         }
-        return false;
-      }
+        return;
+    }
   },
 
   /**
    * Sets the previous node relative to the currently focused item, to focused.
    */
   _focusPrevNode: oncePerAnimationFrame(function () {
     // Start a depth first search and keep going until we reach the currently
     // focused node. Focus the previous node in the DFS, if it exists. If it
     // doesn't exist, we're at the first node already.
 
     let prev;
+    let prevIndex;
 
     const traversal = this._dfsFromRoots();
     const length = traversal.length;
     for (let i = 0; i < length; i++) {
       const item = traversal[i].item;
       if (item === this.props.focused) {
         break;
       }
       prev = item;
+      prevIndex = i;
     }
 
     if (prev === undefined) {
       return;
     }
 
-    this._focus(prev);
+    this._focus(prevIndex, prev);
   }),
 
   /**
    * Handles the down arrow key which will focus either the next child
    * or sibling row.
    */
   _focusNextNode: oncePerAnimationFrame(function () {
     // Start a depth first search and keep going until we reach the currently
@@ -497,23 +543,34 @@ const Tree = module.exports = createClas
     while (i < length) {
       if (traversal[i].item === this.props.focused) {
         break;
       }
       i++;
     }
 
     if (i + 1 < traversal.length) {
-      this._focus(traversal[i + 1].item);
+      this._focus(i + 1, traversal[i + 1].item);
     }
   }),
 
   /**
    * Handles the left arrow key, going back up to the current rows'
    * parent row.
    */
   _focusParentNode: oncePerAnimationFrame(function () {
     const parent = this.props.getParent(this.props.focused);
-    if (parent) {
-      this._focus(parent);
+    if (!parent) {
+      return;
     }
+
+    const traversal = this._dfsFromRoots();
+    const length = traversal.length;
+    let parentIndex = 0;
+    for (; parentIndex < length; parentIndex++) {
+      if (traversal[parentIndex].item === parent) {
+        break;
+      }
+    }
+
+    this._focus(parentIndex, parent);
   }),
 });
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -450,16 +450,17 @@ public final class GeckoProfile {
         if (profileDir != null && !profileDir.isDirectory()) {
             throw new IllegalArgumentException("Profile directory must exist if specified.");
         }
 
         // N.B., mProfileDir can be null at this point.
         mDB = dbFactory.get(profileName, mProfileDir);
     }
 
+    @RobocopTarget
     public BrowserDB getDB() {
         return mDB;
     }
 
 
     // Warning, Changing the lock file state from outside apis will cause this to become out of sync
     public boolean locked() {
         if (mLocked != LockState.UNDEFINED) {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -214,17 +214,18 @@
 <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
 <!ENTITY pref_cookies_disabled "Disabled">
 
 <!ENTITY pref_tap_to_load_images_title2 "Show images">
 <!ENTITY pref_tap_to_load_images_enabled "Always">
 <!ENTITY pref_tap_to_load_images_data "Only over Wi-Fi">
 <!ENTITY pref_tap_to_load_images_disabled2 "Blocked">
 
-<!ENTITY pref_show_web_fonts "Show web fonts">
+<!ENTITY pref_show_web_fonts "Show Web fonts">
+<!ENTITY pref_show_web_fonts_summary "Hide to use default fonts and reduce website load times">
 
 <!ENTITY pref_tracking_protection_title2 "Tracking Protection">
 <!ENTITY pref_tracking_protection_summary3 "Enabled in Private Browsing">
 <!ENTITY pref_donottrack_title "Do not track">
 <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
 
 <!ENTITY pref_tracking_protection_enabled "Enabled">
 <!ENTITY pref_tracking_protection_enabled_pb "Enabled in Private Browsing">
--- a/mobile/android/base/resources/xml/preferences_advanced.xml
+++ b/mobile/android/base/resources/xml/preferences_advanced.xml
@@ -34,17 +34,18 @@
 
     <ListPreference android:key="browser.image_blocking"
                     android:title="@string/pref_tap_to_load_images_title2"
                     android:entries="@array/pref_browser_image_blocking_entries"
                     android:entryValues="@array/pref_browser_image_blocking_values"
                     android:persistent="false" />
 
     <CheckBoxPreference android:key="browser.display.use_document_fonts"
-                        android:title="@string/pref_show_web_fonts" />
+                        android:title="@string/pref_show_web_fonts"
+                        android:summary="@string/pref_show_web_fonts_summary"/>
 
     <ListPreference android:key="plugin.enable"
                     android:title="@string/pref_plugins"
                     android:entries="@array/pref_plugins_entries"
                     android:entryValues="@array/pref_plugins_values"
                     android:persistent="false" />
 
     <SwitchPreference android:key="media.autoplay.enabled"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -202,16 +202,17 @@
   <string name="pref_cookies_disabled">&pref_cookies_disabled;</string>
 
   <string name="pref_tap_to_load_images_title2">&pref_tap_to_load_images_title2;</string>
   <string name="pref_tap_to_load_images_enabled">&pref_tap_to_load_images_enabled;</string>
   <string name="pref_tap_to_load_images_data">&pref_tap_to_load_images_data;</string>
   <string name="pref_tap_to_load_images_disabled2">&pref_tap_to_load_images_disabled2;</string>
 
   <string name="pref_show_web_fonts">&pref_show_web_fonts;</string>
+  <string name="pref_show_web_fonts_summary">&pref_show_web_fonts_summary;</string>
 
   <string name="pref_tracking_protection_title">&pref_tracking_protection_title2;</string>
   <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary3;</string>
   <string name="pref_donottrack_title">&pref_donottrack_title;</string>
   <string name="pref_donottrack_summary">&pref_donottrack_summary;</string>
 
   <string name="pref_tracking_protection_enabled">&pref_tracking_protection_enabled;</string>
   <string name="pref_tracking_protection_enabled_pb">&pref_tracking_protection_enabled_pb;</string>
--- a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
@@ -30,26 +30,26 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "d12adb85d03fff563ff7b799f7e3cbcaa226b6e8ca1ccef64c87ebf828f5ee8cc4d287c6e41070840074ad6afdd6c88d5b14f490f71bb4763a7722e58e373413",
-"size": 38855456
+"digest": "3fd467642a9067a1adfde7e3461b1366912b306607677b213d9f19201c23aab5f7f6361ebea7652bed1565215ed4524b51f5ced83ea68bb51ba2abca09b66148",
+"size": 41591712
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle.tar.xz",
 "unpack": true,
-"digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234",
-"size": 50542140
+"digest": "9011a0a322b55c6f55ca7aa83298886f3f57e2a91a33a079fcdae5af746f9cf1528f36942138cff5a00154757d9acba2f1a4332d0f92da18e454b3c0c1788d20",
+"size": 50805888
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "dotgradle.tar.xz",
 "unpack": true,
 "digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
 "size": 512
--- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
@@ -45,20 +45,20 @@
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "d12adb85d03fff563ff7b799f7e3cbcaa226b6e8ca1ccef64c87ebf828f5ee8cc4d287c6e41070840074ad6afdd6c88d5b14f490f71bb4763a7722e58e373413",
-"size": 38855456
+"digest": "3fd467642a9067a1adfde7e3461b1366912b306607677b213d9f19201c23aab5f7f6361ebea7652bed1565215ed4524b51f5ced83ea68bb51ba2abca09b66148",
+"size": 41591712
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle.tar.xz",
 "unpack": true,
-"digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234",
-"size": 50542140
+"digest": "9011a0a322b55c6f55ca7aa83298886f3f57e2a91a33a079fcdae5af746f9cf1528f36942138cff5a00154757d9acba2f1a4332d0f92da18e454b3c0c1788d20",
+"size": 50805888
 }
 ]
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -56,20 +56,20 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "d12adb85d03fff563ff7b799f7e3cbcaa226b6e8ca1ccef64c87ebf828f5ee8cc4d287c6e41070840074ad6afdd6c88d5b14f490f71bb4763a7722e58e373413",
-"size": 38855456
+"digest": "3fd467642a9067a1adfde7e3461b1366912b306607677b213d9f19201c23aab5f7f6361ebea7652bed1565215ed4524b51f5ced83ea68bb51ba2abca09b66148",
+"size": 41591712
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle.tar.xz",
 "unpack": true,
-"digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234",
-"size": 50542140
+"digest": "9011a0a322b55c6f55ca7aa83298886f3f57e2a91a33a079fcdae5af746f9cf1528f36942138cff5a00154757d9acba2f1a4332d0f92da18e454b3c0c1788d20",
+"size": 50805888
 }
 ]
--- a/netwerk/test/browser/browser.ini
+++ b/netwerk/test/browser/browser.ini
@@ -1,9 +1,10 @@
 [DEFAULT]
 support-files =
   dummy.html
 
 [browser_NetUtil.js]
 [browser_child_resource.js]
+skip-if = e10s && debug && os == "linux" && bits == 64
 [browser_post_file.js]
 [browser_nsIFormPOSTActionChannel.js]
 skip-if = e10s # protocol handler and channel does not work in content process
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -809,17 +809,23 @@ SyncEngine.prototype = {
       }
     });
   },
 
   get previousFailed() {
     return this._previousFailed;
   },
   set previousFailed(val) {
-    let cb = (error) => this._log.error("Failed to set previousFailed", error);
+    let cb = (error) => {
+      if (error) {
+        this._log.error("Failed to set previousFailed", error);
+      } else {
+        this._log.debug("Successfully wrote previousFailed.");
+      }
+    }
     // Coerce the array to a string for more efficient comparison.
     if (val + "" == this._previousFailed) {
       return;
     }
     this._previousFailed = val;
     Utils.namedTimer(function () {
       Utils.jsonSave("failed/" + this.name, this, val, cb);
     }, 0, this, "_previousFailedDelay");
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -21,16 +21,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FxAccounts.jsm");
 
 const CLIENTS_TTL = 1814400; // 21 days
 const CLIENTS_TTL_REFRESH = 604800; // 7 days
 
 const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
 
 function hasDupeCommand(commands, action) {
+  if (!commands) {
+    return false;
+  }
   return commands.some(other => other.command == action.command &&
     Utils.deepEquals(other.args, action.args));
 }
 
 this.ClientsRec = function ClientsRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 ClientsRec.prototype = {
@@ -121,21 +124,29 @@ ClientEngine.prototype = {
   },
 
   get brandName() {
     let brand = new StringBundle("chrome://branding/locale/brand.properties");
     return brand.get("brandShortName");
   },
 
   get localName() {
-    return this.localName = Utils.getDeviceName();
+    let name = Utils.getDeviceName();
+    // If `getDeviceName` returns the default name, set the pref. FxA registers
+    // the device before syncing, so we don't need to update the registration
+    // in this case.
+    Svc.Prefs.set("client.name", name);
+    return name;
   },
   set localName(value) {
     Svc.Prefs.set("client.name", value);
-    fxAccounts.updateDeviceRegistration();
+    // Update the registration in the background.
+    fxAccounts.updateDeviceRegistration().catch(error => {
+      this._log.warn("failed to update fxa device registration", error);
+    });
   },
 
   get localType() {
     return Utils.getDeviceType();
   },
   set localType(value) {
     Svc.Prefs.set("client.type", value);
   },
@@ -175,16 +186,21 @@ ClientEngine.prototype = {
       for (let staleID of staleIDs) {
         this._removeRemoteClient(staleID);
       }
     } finally {
       this._incomingClients = null;
     }
   },
 
+  _uploadOutgoing() {
+    this._clearedCommands = null;
+    SyncEngine.prototype._uploadOutgoing.call(this);
+  },
+
   _syncFinish() {
     // Record telemetry for our device types.
     for (let [deviceType, count] of this.deviceTypes) {
       let hid;
       switch (deviceType) {
         case "desktop":
           hid = "WEAVE_DEVICE_COUNT_DESKTOP";
           break;
@@ -220,16 +236,17 @@ ClientEngine.prototype = {
 
   // Treat reset the same as wiping for locally cached clients
   _resetClient() {
     this._wipeClient();
   },
 
   _wipeClient: function _wipeClient() {
     SyncEngine.prototype._resetClient.call(this);
+    delete this.localCommands;
     this._store.wipe();
   },
 
   removeClientData: function removeClientData() {
     let res = this.service.resource(this.engineURL + "/" + this.localID);
     res.delete();
   },
 
@@ -262,16 +279,22 @@ ClientEngine.prototype = {
     logout:      { args: 0, desc: "Log out client" },
     displayURI:  { args: 3, desc: "Instruct a client to display a URI" },
   },
 
   /**
    * Remove any commands for the local client and mark it for upload.
    */
   clearCommands: function clearCommands() {
+    if (!this._clearedCommands) {
+      this._clearedCommands = [];
+    }
+    // Keep track of cleared local commands until the next sync, so that we
+    // don't reupload them.
+    this._clearedCommands = this._clearedCommands.concat(this.localCommands);
     delete this.localCommands;
     this._tracker.addChangedID(this.localID);
   },
 
   /**
    * Sends a command+args pair to a specific client.
    *
    * @param command Command string
@@ -461,31 +484,66 @@ function ClientStore(name, engine) {
 ClientStore.prototype = {
   __proto__: Store.prototype,
 
   create(record) {
     this.update(record)
   },
 
   update: function update(record) {
+    if (record.id == this.engine.localID) {
+      this._updateLocalRecord(record);
+    } else {
+      this._updateRemoteRecord(record);
+    }
+  },
+
+  _updateLocalRecord(record) {
+    // Local changes for our client means we're clearing commands or
+    // uploading a new record.
+    let incomingCommands = record.commands;
+    if (incomingCommands) {
+      // Filter out incoming commands that we've cleared.
+      incomingCommands = incomingCommands.filter(action =>
+        !hasDupeCommand(this.engine._clearedCommands, action));
+      if (!incomingCommands.length) {
+        // Use `undefined` instead of `null` to avoid creating a null field
+        // in the uploaded record.
+        incomingCommands = undefined;
+      }
+    }
     // Only grab commands from the server; local name/type always wins
-    if (record.id == this.engine.localID)
-      this.engine.localCommands = record.commands;
-    else {
-      let currentRecord = this._remoteClients[record.id];
-      if (currentRecord && currentRecord.commands) {
-        // Merge commands.
-        for (let action of currentRecord.commands) {
-          if (!hasDupeCommand(record.cleartext.commands, action)) {
-            record.cleartext.commands.push(action);
-          }
-        }
+    this.engine.localCommands = incomingCommands;
+  },
+
+  _updateRemoteRecord(record) {
+    let currentRecord = this._remoteClients[record.id];
+    if (!currentRecord || !currentRecord.commands ||
+        !(record.id in this.engine._modified)) {
+
+      // If we have a new incoming record or no outgoing commands, use the
+      // full incoming record from the server.
+      this._remoteClients[record.id] = record.cleartext;
+      return;
+    }
+
+    // Otherwise, we have outgoing commands for a client, so merge them
+    // with the commands that we downloaded from the server.
+    for (let action of currentRecord.commands) {
+      if (hasDupeCommand(record.cleartext.commands, action)) {
+        // Ignore commands the server already knows about.
+        continue;
       }
-      this._remoteClients[record.id] = record.cleartext;
+      if (record.cleartext.commands) {
+        record.cleartext.commands.push(action);
+      } else {
+        record.cleartext.commands = [action];
+      }
     }
+    this._remoteClients[record.id] = record.cleartext;
   },
 
   createRecord: function createRecord(id, collection) {
     let record = new ClientsRec(collection, id);
 
     // Package the individual components into a record for the local client
     if (id == this.engine.localID) {
       let cb = Async.makeSpinningCallback();
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -622,16 +622,20 @@ add_test(function test_send_uri_to_clien
   try {
     engine.sendURIToClientForDisplay(uri, unknownId);
   } catch (ex) {
     error = ex;
   }
 
   do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
 
+  Svc.Prefs.resetBranch("");
+  Service.recordManager.clearCache();
+  engine._resetClient();
+
   run_next_test();
 });
 
 add_test(function test_receive_display_uri() {
   _("Ensure processing of received 'displayURI' commands works.");
 
   // We don't set up WBOs and perform syncing because other tests verify
   // the command API works as advertised. This saves us a little work.
@@ -661,18 +665,19 @@ add_test(function test_receive_display_u
 
     run_next_test();
   };
 
   Svc.Obs.add(ev, handler);
 
   do_check_true(engine.processIncomingCommands());
 
+  Svc.Prefs.resetBranch("");
+  Service.recordManager.clearCache();
   engine._resetClient();
-  run_next_test();
 });
 
 add_test(function test_optional_client_fields() {
   _("Ensure that we produce records with the fields added in Bug 1097222.");
 
   const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
   let local = engine._store.createRecord(engine.localID, "clients");
   do_check_eq(local.name, engine.localName);
@@ -688,16 +693,17 @@ add_test(function test_optional_client_f
   // ... and also that they're non-empty.
   do_check_true(!!local.os);
   do_check_true(!!local.appPackage);
   do_check_true(!!local.application);
 
   // We don't currently populate device or formfactor.
   // See Bug 1100722, Bug 1100723.
 
+  engine._resetClient();
   run_next_test();
 });
 
 add_test(function test_merge_commands() {
   _("Verifies local commands for remote clients are merged with the server's");
 
   let now = Date.now() / 1000;
   let contents = {
@@ -839,13 +845,72 @@ add_test(function test_deleted_commands(
     try {
       server.deleteCollections("foo");
     } finally {
       server.stop(run_next_test);
     }
   }
 });
 
+add_test(function test_send_uri_ack() {
+  _("Ensure a sent URI is deleted when the client syncs");
+
+  let now = Date.now() / 1000;
+  let contents = {
+    meta: {global: {engines: {clients: {version: engine.version,
+                                        syncID: engine.syncID}}}},
+    clients: {},
+    crypto: {}
+  };
+  let server = serverForUsers({"foo": "password"}, contents);
+  let user   = server.user("foo");
+
+  new SyncTestingInfrastructure(server.server);
+  generateNewKeys(Service.collectionKeys);
+
+  try {
+    let fakeSenderID = Utils.makeGUID();
+
+    _("Initial sync for empty clients collection");
+    engine._sync();
+    let collection = server.getCollection("foo", "clients");
+    let ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
+    ok(ourPayload, "Should upload our client record");
+
+    _("Send a URL to the device on the server");
+    ourPayload.commands = [{
+      command: "displayURI",
+      args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
+    }];
+    server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload(ourPayload), now));
+
+    _("Sync again");
+    engine._sync();
+    deepEqual(engine.localCommands, [{
+      command: "displayURI",
+      args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
+    }], "Should receive incoming URI");
+    ok(engine.processIncomingCommands(), "Should process incoming commands");
+    ok(!engine.localCommands, "Should clear commands after processing");
+
+    _("Check that the command was removed on the server");
+    engine._sync();
+    ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
+    ok(ourPayload, "Should upload the synced client record");
+    ok(!ourPayload.commands, "Should not reupload cleared commands");
+  } finally {
+    Svc.Prefs.resetBranch("");
+    Service.recordManager.clearCache();
+    engine._resetClient();
+
+    try {
+      server.deleteCollections("foo");
+    } finally {
+      server.stop(run_next_test);
+    }
+  }
+});
+
 function run_test() {
   initTestLogging("Trace");
   Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace;
   run_next_test();
 }
--- a/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_gradle_dependencies.py
+++ b/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_gradle_dependencies.py
@@ -5,11 +5,12 @@ config = {
     'src_mozconfig': 'mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly',
     'tooltool_manifest_src': 'mobile/android/config/tooltool-manifests/android-gradle-dependencies/releng.manifest',
     'multi_locale_config_platform': 'android',
     'postflight_build_mach_commands': [
         ['gradle',
          'assembleAutomationRelease',
          'assembleAutomationDebug',
          'assembleAutomationDebugAndroidTest',
+         'checkstyle',
         ],
     ],
 }
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/builds/releng_sub_android_configs/64_checkstyle.py
@@ -0,0 +1,11 @@
+config = {
+    'base_name': 'Android checkstyle %(branch)s',
+    'stage_platform': 'android-checkstyle',
+    'build_type': 'api-15-opt',
+    'src_mozconfig': 'mobile/android/config/mozconfigs/android-api-15-frontend/nightly',
+    'tooltool_manifest_src': 'mobile/android/config/tooltool-manifests/android-frontend/releng.manifest',
+    'multi_locale_config_platform': 'android',
+    'postflight_build_mach_commands': [
+        ['gradle', 'app:checkstyle'],
+    ],
+}
--- a/testing/mozharness/mozharness/mozilla/building/buildbase.py
+++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py
@@ -360,16 +360,17 @@ class BuildOptionParser(object):
         'api-15-gradle-dependencies': 'builds/releng_sub_%s_configs/%s_api_15_gradle_dependencies.py',
         'api-15': 'builds/releng_sub_%s_configs/%s_api_15.py',
         'api-9-debug': 'builds/releng_sub_%s_configs/%s_api_9_debug.py',
         'api-11-debug': 'builds/releng_sub_%s_configs/%s_api_11_debug.py',
         'api-15-debug': 'builds/releng_sub_%s_configs/%s_api_15_debug.py',
         'x86': 'builds/releng_sub_%s_configs/%s_x86.py',
         'api-11-partner-sample1': 'builds/releng_sub_%s_configs/%s_api_11_partner_sample1.py',
         'api-15-partner-sample1': 'builds/releng_sub_%s_configs/%s_api_15_partner_sample1.py',
+        'android-checkstyle': 'builds/releng_sub_%s_configs/%s_checkstyle.py',
         'android-lint': 'builds/releng_sub_%s_configs/%s_lint.py',
     }
     build_pool_cfg_file = 'builds/build_pool_specifics.py'
     branch_cfg_file = 'builds/branch_specifics.py'
 
     @classmethod
     def _query_pltfrm_and_bits(cls, target_option, options):
         """ determine platform and bits
--- a/testing/taskcluster/tasks/branches/base_jobs.yml
+++ b/testing/taskcluster/tasks/branches/base_jobs.yml
@@ -292,17 +292,26 @@ tasks:
         # Other misc lint related files.
         - 'tools/lint/**'
   android-api-15-gradle-dependencies:
     task: tasks/builds/android_api_15_gradle_dependencies.yml
     root: true
     when:
       file_patterns:
         - 'testing/docker/android-gradle-build/**'
+        - 'testing/mozharness/configs/builds/releng_sub_android_configs/*gradle_dependencies.py'
         - '**/*.gradle'
+  android-checkstyle:
+    task: tasks/builds/android_checkstyle.yml
+    root: true
+    when:
+      file_patterns:
+        - 'mobile/android/**/checkstyle.xml'
+        - 'mobile/android/**/*.gradle'
+        - 'mobile/android/**/*.java'
   android-lint:
     task: tasks/builds/android_lint.yml
     root: true
     when:
       file_patterns:
         - 'mobile/android/**/*.java'
         - 'mobile/android/**/*.jpeg'
         - 'mobile/android/**/*.jpg'
new file mode 100644
--- /dev/null
+++ b/testing/taskcluster/tasks/builds/android_checkstyle.yml
@@ -0,0 +1,72 @@
+$inherits:
+  from: 'tasks/builds/mobile_base.yml'
+  variables:
+    build_name: 'android-checkstyle'
+    build_type: 'opt'
+task:
+  metadata:
+      name: '[TC] Android checkstyle'
+      description: 'Android checkstyle'
+
+  workerType: android-api-15
+
+  routes:
+    - 'index.buildbot.branches.{{project}}.android-checkstyle'
+    - 'index.buildbot.revisions.{{head_rev}}.{{project}}.android-checkstyle'
+
+  scopes:
+    - 'docker-worker:cache:level-{{level}}-{{project}}-build-android-checkstyle-workspace'
+    - 'docker-worker:cache:tooltool-cache'
+    - 'docker-worker:relengapi-proxy:tooltool.download.internal'
+    - 'docker-worker:relengapi-proxy:tooltool.download.public'
+
+  payload:
+    cache:
+      level-{{level}}-{{project}}-build-android-checkstyle-workspace: '/home/worker/workspace'
+      tooltool-cache: '/home/worker/tooltool-cache'
+
+    features:
+      relengAPIProxy: true
+
+    env:
+      # inputs to mozharness
+      MOZHARNESS_SCRIPT: 'mozharness/scripts/fx_desktop_build.py'
+      # TODO: make these additional configuration files go away
+      MOZHARNESS_CONFIG: >
+          builds/releng_base_android_64_builds.py
+          disable_signing.py
+          platform_supports_post_upload_to_latest.py
+      MOZHARNESS_ACTIONS: "get-secrets build multi-l10n update"
+      MH_CUSTOM_BUILD_VARIANT_CFG: android-checkstyle
+      MH_BRANCH: {{project}}
+      MH_BUILD_POOL: taskcluster
+      GRADLE_USER_HOME: '/home/worker/workspace/build/src/dotgradle'
+
+    maxRunTime: 36000
+
+    command: ["/bin/bash", "bin/build.sh"]
+
+    artifacts:
+      'public/android/checkstyle/checkstyle.xml':
+        type: file
+        path: '/home/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/reports/checkstyle/checkstyle.xml'
+        expires: '{{#from_now}}1 year{{/from_now}}'
+
+  extra:
+    treeherderEnv:
+      - production
+      - staging
+    treeherder:
+      machine:
+        # see https://github.com/mozilla/treeherder/blob/master/ui/js/values.js
+        platform: android-4-0-armv7-api15
+      groupSymbol: tc
+      groupName: Submitted by taskcluster
+      symbol: checkstyle
+      tier: 2
+    # Rather then enforcing particular conventions we require that all build
+    # tasks provide the "build" extra field to specify where the build and tests
+    # files are located.
+    locations:
+      build: 'public/build/target.linux-x86_64.tar.bz2'
+      tests: 'public/build/target.tests.zip'
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -54,19 +54,16 @@ const QUERYTYPE_FILTERED            = 0;
 const QUERYTYPE_AUTOFILL_HOST       = 1;
 const QUERYTYPE_AUTOFILL_URL        = 2;
 
 // This separator is used as an RTL-friendly way to split the title and tags.
 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
 // "comment" back into the title and the tag.
 const TITLE_TAGS_SEPARATOR = " \u2013 ";
 
-// This separator identifies the search engine name in the title.
-const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 ";
-
 // Telemetry probes.
 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
 const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
 // The default frecency value used when inserting matches with unknown frecency.
 const FRECENCY_DEFAULT = 1000;
 
 // Remote matches are appended when local matches are below a given frecency
 // threshold (FRECENCY_DEFAULT) as soon as they arrive.  However we'll
@@ -1380,20 +1377,25 @@ Search.prototype = {
     // URL. For example, "https://www.google.com/search?q=terms&client=firefox"
     // when searching for "Firefox".
     let terms = parseResult.terms.toLowerCase();
     if (this._searchTokens.length > 0 &&
         this._searchTokens.every(token => !terms.includes(token))) {
       return;
     }
 
-    // Use the special separator that the binding will use to style the item.
-    match.style = "search " + match.style;
-    match.comment = parseResult.terms + TITLE_SEARCH_ENGINE_SEPARATOR +
-                    parseResult.engineName;
+    // Turn the match into a searchengine action with a favicon.
+    match.value = makeActionURL("searchengine", {
+      engineName: parseResult.engineName,
+      input: parseResult.terms,
+      searchQuery: parseResult.terms,
+    });
+    match.comment = parseResult.engineName;
+    match.icon = match.icon || match.iconUrl;
+    match.style = "action searchengine favicon";
   },
 
   _addMatch(match) {
     // A search could be canceled between a query start and its completion,
     // in such a case ensure we won't notify any result for it.
     if (!this.pending)
       return;
 
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -109,18 +109,16 @@ AutoCompleteInput.prototype = {
 }
 
 // A helper for check_autocomplete to check a specific match against data from
 // the controller.
 function _check_autocomplete_matches(match, result) {
   let { uri, title, tags, searchEngine, style } = match;
   if (tags)
     title += " \u2013 " + tags.sort().join(", ");
-  if (searchEngine)
-    title += TITLE_SEARCH_ENGINE_SEPARATOR + searchEngine;
   if (style)
     style = style.sort();
   else
     style = ["favicon"];
 
   do_print("Checking against expected '" + uri.spec + "', '" + title + "'...");
   // Got a match on both uri and title?
   if (stripPrefix(uri.spec) != stripPrefix(result.value) || title != result.comment) {
@@ -366,16 +364,19 @@ function makeSearchMatch(input, extra = 
     searchQuery: "searchQuery" in extra ? extra.searchQuery : input,
   };
   if ("alias" in extra) {
     // May be undefined, which is expected, but in that case make sure it's not
     // included in the params of the moz-action URL.
     params.alias = extra.alias;
   }
   let style = [ "action", "searchengine" ];
+  if (Array.isArray(extra.style)) {
+    style.push(...extra.style);
+  }
   if (extra.heuristic) {
     style.push("heuristic");
   }
   return {
     uri: makeActionURI("searchengine", params),
     title: params.engineName,
     style,
   }
--- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js
@@ -13,18 +13,27 @@ add_task(function* test_searchEngine() {
   let uri2 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=2");
   yield PlacesTestUtils.addVisits({ uri: uri1, title: "Terms - SearchEngine Search" });
   yield addBookmark({ uri: uri2, title: "Terms - SearchEngine Search" });
 
   do_print("Past search terms should be styled, unless bookmarked");
   Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
   yield check_autocomplete({
     search: "term",
-    matches: [ { uri: uri1, title: "Terms", searchEngine: "SearchEngine", style: ["favicon", "search"] },
-               { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
+    matches: [
+      makeSearchMatch("Terms", {
+        engineName: "SearchEngine",
+        style: ["favicon"]
+      }),
+      {
+        uri: uri2,
+        title: "Terms - SearchEngine Search",
+        style: ["bookmark"]
+      }
+    ]
   });
 
   do_print("Past search terms should not be styled if restyling is disabled");
   Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false);
   yield check_autocomplete({
     search: "term",
     matches: [ { uri: uri1, title: "Terms - SearchEngine Search" },
                { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
--- a/toolkit/content/autocomplete.css
+++ b/toolkit/content/autocomplete.css
@@ -7,11 +7,29 @@
 
 /* Apply crisp rendering for favicons at exactly 2dppx resolution */
 @media (resolution: 2dppx) {
   .ac-site-icon {
     image-rendering: -moz-crisp-edges;
   }
 }
 
-richlistitem > .ac-title-box > .ac-title > .ac-comment:not([selected]) > html|span.ac-selected-text {
+richlistitem {
+  -moz-box-orient: horizontal;
+  overflow: hidden;
+}
+
+.ac-title-text,
+.ac-tags-text,
+.ac-url-text,
+.ac-action-text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.ac-tags[empty] {
   display: none;
 }
+
+.ac-action[actiontype=searchengine]:not([selected]) {
+  display: none;
+}
--- a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
@@ -133,25 +133,39 @@ function nextTest() {
   autocomplete.value = currentTest.search;
   synthesizeKey("VK_DOWN", {});
 }
 
 function checkSearchCompleted() {
   let autocomplete = $("richautocomplete");
   let result = autocomplete.popup.richlistbox.firstChild;
 
-  for (let attribute of [result._title, result._url]) {
-    is(attribute.childNodes.length, currentTest.emphasis.length, "The element should have the expected number of children.");
+  for (let attribute of [result._titleText, result._urlText]) {
+
+    let numChildren = currentTest.emphasis.length;
+    let childNodeStart = 0;
+    if (attribute == result._urlText) {
+      // For the URL description, the first child is the em dash separator that
+      // visually separates the the title string from the URL string.
+      numChildren++;
+      childNodeStart = 1;
+      let node = attribute.childNodes[0];
+      ok(node.classList.contains("ac-title-urlaction-separator"),
+         "First child of URL text should be separator");
+    }
+
+    is(attribute.childNodes.length, numChildren, "The element should have the expected number of children.");
+
     for (let i = 0; i < currentTest.emphasis.length; i++) {
-      let node = attribute.childNodes[i];
+      let node = attribute.childNodes[childNodeStart + i];
       // Emphasized parts strictly alternate.
       if ((i % 2 == 0) == currentTest.emphasizeFirst) {
         // Check that this part is correctly emphasized.
         is(node.nodeName, "span", ". That child should be a span node");
-        is(node.className, "ac-emphasize-text", ". That child should be emphasized");
+        ok(node.classList.contains("ac-emphasize-text"), ". That child should be emphasized");
         is(node.textContent, currentTest.emphasis[i], ". That emphasis should be as expected.");
       } else {
         // Check that this part is _not_ emphasized.
         is(node.nodeName, "#text", ". That child should be a text node");
         is(node.textContent, currentTest.emphasis[i], ". That text should be as expected.");
       }
     }
   }
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1164,26 +1164,32 @@ extends="chrome://global/content/binding
 
           // Default the height to 0 if we have no rows to show
           let height = 0;
           if (numRows) {
             if (!this._rowHeight) {
               let firstRowRect = rows[0].getBoundingClientRect();
               this._rowHeight = firstRowRect.height;
 
-              let transition =
-                window.getComputedStyle(this.richlistbox).transitionProperty;
+              let style = window.getComputedStyle(this.richlistbox);
+
+              let transition = style.transitionProperty;
               this._rlbAnimated = transition && transition != "none";
 
+              let paddingTop = parseInt(style.paddingTop) || 0;
+              let paddingBottom = parseInt(style.paddingBottom) || 0;
+              this._rlbPadding = paddingTop + paddingBottom;
+
               // Set a fixed max-height to avoid flicker when growing the panel.
-              this.richlistbox.style.maxHeight = (this._rowHeight * this.maxRows) + "px";
+              this.richlistbox.style.maxHeight =
+                ((this._rowHeight * this.maxRows) + this._rlbPadding) + "px";
             }
 
             // Calculate the height to have the first row to last row shown
-            height = this._rowHeight * numRows;
+            height = (this._rowHeight * numRows) + this._rlbPadding;
           }
 
           let animate = this._rlbAnimated &&
                         this.getAttribute("dontanimate") != "true";
           let currentHeight = this.richlistbox.getBoundingClientRect().height;
           if (height > currentHeight) {
             // Grow immediately.
             if (animate) {
@@ -1318,82 +1324,91 @@ extends="chrome://global/content/binding
       <property name="view"
                 onget="return this.mInput.controller;"
                 onset="return val;"/>
 
     </implementation>
   </binding>
 
   <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-    <content>
-      <xul:hbox align="center" class="ac-title-box">
-        <xul:image xbl:inherits="src=image" class="ac-site-icon"/>
-        <xul:hbox anonid="title-box" class="ac-title" flex="1"
-                  onunderflow="_doUnderflow('_title');"
-                  onoverflow="_doOverflow('_title');">
-          <xul:description anonid="title" class="ac-normal-text ac-comment" xbl:inherits="selected"/>
-        </xul:hbox>
-        <xul:label anonid="title-overflow-ellipsis" xbl:inherits="selected"
-                   class="ac-ellipsis-after ac-comment"/>
-        <xul:hbox anonid="extra-box" class="ac-extra" align="center" hidden="true">
-          <xul:image class="ac-result-type-tag"/>
-          <xul:label class="ac-normal-text ac-comment" xbl:inherits="selected" value=":"/>
-          <xul:description anonid="extra" class="ac-normal-text ac-comment" xbl:inherits="selected"/>
-        </xul:hbox>
-        <xul:image anonid="type-image" class="ac-type-icon" xbl:inherits="selected"/>
+
+    <content align="center"
+             onoverflow="this._onOverflow();"
+             onunderflow="this._onUnderflow();">
+      <xul:image anonid="type-icon"
+                 class="ac-type-icon"
+                 xbl:inherits="selected,current"/>
+      <xul:image anonid="site-icon"
+                 class="ac-site-icon"
+                 xbl:inherits="src=image,selected"/>
+      <xul:hbox class="ac-title"
+                align="center"
+                xbl:inherits="selected">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="title-text"
+                           class="ac-title-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
       </xul:hbox>
-      <xul:hbox align="center" class="ac-url-box">
-        <xul:spacer class="ac-site-icon"/>
-        <xul:image class="ac-action-icon"/>
-        <xul:hbox anonid="url-box" class="ac-url" flex="1"
-                  onunderflow="_doUnderflow('_url');"
-                  onoverflow="_doOverflow('_url');">
-          <xul:description anonid="url" class="ac-normal-text ac-url-text"
-                           xbl:inherits="selected type"/>
-          <xul:description anonid="action" class="ac-normal-text ac-action-text"
-                           xbl:inherits="selected type"/>
-        </xul:hbox>
-        <xul:label anonid="url-overflow-ellipsis" xbl:inherits="selected"
-                   class="ac-ellipsis-after ac-url-text"/>
-        <xul:spacer class="ac-type-icon"/>
+      <xul:hbox anonid="tags"
+                class="ac-tags"
+                align="center"
+                xbl:inherits="selected">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="tags-text"
+                           class="ac-tags-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
+      </xul:hbox>
+      <xul:hbox class="ac-url"
+                align="center"
+                xbl:inherits="selected,actiontype">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="url-text"
+                           class="ac-url-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
+      </xul:hbox>
+      <xul:hbox class="ac-action"
+                align="center"
+                xbl:inherits="selected,actiontype">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="action-text"
+                           class="ac-action-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
       </xul:hbox>
     </content>
+
     <implementation implements="nsIDOMXULSelectControlItemElement">
       <constructor>
         <![CDATA[
-            let ellipsis = "\u2026";
-            try {
-              ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
-                getService(Components.interfaces.nsIPrefBranch).
-                getComplexValue("intl.ellipsis",
-                  Components.interfaces.nsIPrefLocalizedString).data;
-            } catch (ex) {
-              // Do nothing.. we already have a default
-            }
-
-            this._urlOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "url-overflow-ellipsis");
-            this._titleOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "title-overflow-ellipsis");
-
-            this._urlOverflowEllipsis.value = ellipsis;
-            this._titleOverflowEllipsis.value = ellipsis;
-
-            this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
-
-            this._urlBox = document.getAnonymousElementByAttribute(this, "anonid", "url-box");
-            this._url = document.getAnonymousElementByAttribute(this, "anonid", "url");
-            this._action = document.getAnonymousElementByAttribute(this, "anonid", "action");
-
-            this._titleBox = document.getAnonymousElementByAttribute(this, "anonid", "title-box");
-            this._title = document.getAnonymousElementByAttribute(this, "anonid", "title");
-
-            this._extraBox = document.getAnonymousElementByAttribute(this, "anonid", "extra-box");
-            this._extra = document.getAnonymousElementByAttribute(this, "anonid", "extra");
-
-            this._adjustAcItem();
-          ]]>
+          this._typeIcon = document.getAnonymousElementByAttribute(
+            this, "anonid", "type-icon"
+          );
+          this._siteIcon = document.getAnonymousElementByAttribute(
+            this, "anonid", "site-icon"
+          );
+          this._titleText = document.getAnonymousElementByAttribute(
+            this, "anonid", "title-text"
+          );
+          this._tags = document.getAnonymousElementByAttribute(
+            this, "anonid", "tags"
+          );
+          this._tagsText = document.getAnonymousElementByAttribute(
+            this, "anonid", "tags-text"
+          );
+          this._urlText = document.getAnonymousElementByAttribute(
+            this, "anonid", "url-text"
+          );
+          this._actionText = document.getAnonymousElementByAttribute(
+            this, "anonid", "action-text"
+          );
+          this._adjustAcItem();
+        ]]>
       </constructor>
 
       <property name="label" readonly="true">
         <getter>
           <![CDATA[
             // This property is a string that is read aloud by screen readers,
             // so it must not contain anything that should not be user-facing.
 
@@ -1435,16 +1450,36 @@ extends="chrome://global/content/binding
               getService(Components.interfaces.nsIPrefBranch).
               getIntPref("toolkit.autocomplete.richBoundaryCutoff");
           }
           return this._boundaryCutoff;
           ]]>
         </getter>
       </property>
 
+      <field name="_inOverflow">false</field>
+
+      <method name="_onOverflow">
+        <body>
+          <![CDATA[
+          this._inOverflow = true;
+          this._handleOverflow();
+          ]]>
+        </body>
+      </method>
+
+      <method name="_onUnderflow">
+        <body>
+          <![CDATA[
+          this._inOverflow = false;
+          this._handleOverflow();
+          ]]>
+        </body>
+      </method>
+
       <method name="_getBoundaryIndices">
         <parameter name="aText"/>
         <parameter name="aSearchTokens"/>
         <body>
           <![CDATA[
           // Short circuit for empty search ([""] == "")
           if (aSearchTokens == "")
             return [0, aText.length];
@@ -1519,46 +1554,133 @@ extends="chrome://global/content/binding
         <parameter name="aText"/>
         <parameter name="aNoEmphasis"/>
         <body>
           <![CDATA[
           // Get rid of all previous text
           while (aDescriptionElement.hasChildNodes())
             aDescriptionElement.removeChild(aDescriptionElement.firstChild);
 
+          // Add a separator to the front of the URL and action.
+          if (aText &&
+              (aDescriptionElement == this._urlText ||
+               aDescriptionElement == this._actionText)) {
+            let span =
+              document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+            aDescriptionElement.appendChild(span);
+            span.className = "ac-title-urlaction-separator";
+            span.textContent = "—";
+          }
+
           // If aNoEmphasis is specified, don't add any emphasis
           if (aNoEmphasis) {
             aDescriptionElement.appendChild(document.createTextNode(aText));
             return;
           }
 
           // Get the indices that separate match and non-match text
           let search = this.getAttribute("text");
           let tokens = this._getSearchTokens(search);
           let indices = this._getBoundaryIndices(aText, tokens);
 
+          this._appendDescriptionSpans(indices, aText, aDescriptionElement,
+                                       aDescriptionElement);
+          ]]>
+        </body>
+      </method>
+
+      <method name="_appendDescriptionSpans">
+        <parameter name="indices"/>
+        <parameter name="text"/>
+        <parameter name="spansParentElement"/>
+        <parameter name="descriptionElement"/>
+        <body>
+          <![CDATA[
           let next;
           let start = 0;
           let len = indices.length;
           // Even indexed boundaries are matches, so skip the 0th if it's empty
           for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
             next = indices[i];
-            let text = aText.substr(start, next - start);
+            let spanText = text.substr(start, next - start);
             start = next;
 
             if (i % 2 == 0) {
               // Emphasize the text for even indices
-              let span = aDescriptionElement.appendChild(
+              let span = spansParentElement.appendChild(
                 document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
-              span.className = "ac-emphasize-text";
-              span.textContent = text;
+              this._setUpEmphasisSpan(span, descriptionElement);
+              span.textContent = spanText;
             } else {
               // Otherwise, it's plain text
-              aDescriptionElement.appendChild(document.createTextNode(text));
+              spansParentElement.appendChild(document.createTextNode(spanText));
+            }
+          }
+          ]]>
+        </body>
+      </method>
+
+      <method name="_setUpTags">
+        <parameter name="tags"/>
+        <body>
+          <![CDATA[
+          while (this._tagsText.hasChildNodes()) {
+            this._tagsText.firstChild.remove();
+          }
+
+          let anyTagsMatch = false;
+
+          // Include only tags that match the search string.
+          for (let tag of tags) {
+            // Check if the tag matches the search string.
+            let search = this.getAttribute("text");
+            let tokens = this._getSearchTokens(search);
+            let indices = this._getBoundaryIndices(tag, tokens);
+
+            if (indices.length == 2 &&
+                indices[0] == 0 &&
+                indices[1] == tag.length) {
+              // The tag doesn't match the search string, so don't include it.
+              continue;
             }
+
+            anyTagsMatch = true;
+
+            let tagSpan =
+              document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+            tagSpan.classList.add("ac-tag");
+            this._tagsText.appendChild(tagSpan);
+
+            this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
+          }
+
+          return anyTagsMatch;
+          ]]>
+        </body>
+      </method>
+
+      <method name="_setUpEmphasisSpan">
+        <parameter name="aSpan"/>
+        <parameter name="aDescriptionElement"/>
+        <body>
+          <![CDATA[
+          aSpan.classList.add("ac-emphasize-text");
+          switch (aDescriptionElement) {
+            case this._titleText:
+              aSpan.classList.add("ac-emphasize-text-title");
+              break;
+            case this._tagsText:
+              aSpan.classList.add("ac-emphasize-text-tag");
+              break;
+            case this._urlText:
+              aSpan.classList.add("ac-emphasize-text-url");
+              break;
+            case this._actionText:
+              aSpan.classList.add("ac-emphasize-text-action");
+              break;
           }
           ]]>
         </body>
       </method>
 
       <!--
         This will generate an array of emphasis pairs for use with
         _setUpEmphasisedSections(). Each pair is a tuple (array) that
@@ -1647,20 +1769,17 @@ extends="chrome://global/content/binding
 
           for (let [text, emphasise] of aTextPairs) {
             if (emphasise) {
               let span = aDescriptionElement.appendChild(
                 document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
               span.textContent = text;
               switch(emphasise) {
                 case "match":
-                  span.className = "ac-emphasize-text";
-                  break;
-                case "selected":
-                  span.className = "ac-selected-text";
+                  this._setUpEmphasisSpan(span, aDescriptionElement);
                   break;
               }
             } else {
               aDescriptionElement.appendChild(document.createTextNode(text));
             }
           }
           ]]>
         </body>
@@ -1679,76 +1798,78 @@ extends="chrome://global/content/binding
           return this._textToSubURI.unEscapeURIForUI("UTF-8", url);
           ]]>
         </body>
       </method>
 
       <method name="_adjustAcItem">
         <body>
           <![CDATA[
-          let originalUrl = this.getAttribute("url");
+          this._titleText.style.removeProperty("max-width");
+          this._tagsText.style.removeProperty("max-width");
+          this._urlText.style.removeProperty("max-width");
+          this._actionText.style.removeProperty("max-width");
+
           let title = this.getAttribute("title");
-          let type = this.getAttribute("type");
 
           let displayUrl;
-          let emphasiseTitle = true;
+          let originalUrl = this.getAttribute("url");
           let emphasiseUrl = true;
 
-          // Hide the title's extra box by default, until we find out later if
-          // we need extra stuff.
-          this._extraBox.hidden = true;
-          this._titleBox.flex = 1;
-          this._typeImage.hidden = false;
+          let type = this.getAttribute("type");
+          let types = new Set(type.split(/\s+/));
+          let initialTypes = new Set(types);
+          // Remove types that should ultimately not be in the `type` string.
+          types.delete("action");
+          types.delete("autofill");
+          types.delete("heuristic");
+          type = [...types][0] || "";
+
+          let action;
+
+          if (initialTypes.has("autofill")) {
+            // Treat autofills as visiturl actions.
+            action = {
+              type: "visiturl",
+              params: {
+                url: originalUrl,
+              },
+            };
+          }
 
           this.removeAttribute("actiontype");
           this.classList.remove("overridable-action");
 
-          // The ellipses are hidden via their visibility so that they always
-          // take up space and don't pop in on top of text when shown.  For
-          // keyword searches, however, the title ellipsis should not take up
-          // space when hidden.  Setting the hidden property accomplishes that.
-          this._titleOverflowEllipsis.hidden = false;
-
-          let types = new Set(type.split(/\s+/));
-
-          // Remove types that should ultimately not be in the `type` string.
-          let initialTypes = new Set(types);
-          types.delete("action");
-          types.delete("autofill");
-          types.delete("heuristic");
-          types.delete("search");
-
-          type = [...types][0] || "";
-
           // If the type includes an action, set up the item appropriately.
-          if (initialTypes.has("action")) {
-            let action = this._parseActionUrl(originalUrl);
+          if (initialTypes.has("action") || action) {
+            action = action || this._parseActionUrl(originalUrl);
             this.setAttribute("actiontype", action.type);
 
             if (action.type == "switchtab") {
               this.classList.add("overridable-action");
               displayUrl = this._unescapeUrl(action.params.url);
-              let desc = this._stringBundle.GetStringFromName("switchToTab");
-              this._setUpDescription(this._action, desc, true);
+              let desc = this._stringBundle.GetStringFromName("switchToTab2");
+              this._setUpDescription(this._actionText, desc, true);
             } else if (action.type == "remotetab") {
               displayUrl = this._unescapeUrl(action.params.url);
               let desc = action.params.deviceName;
-              this._setUpDescription(this._action, desc, true);
+              this._setUpDescription(this._actionText, desc, true);
             } else if (action.type == "searchengine") {
               emphasiseUrl = false;
 
               // The order here is not localizable, we default to appending
               // "- Search with Engine" to the search string, to be able to
               // properly generate emphasis pairs. That said, no localization
               // changed the order while it was possible, so doesn't look like
               // there's a strong need for that.
               let {engineName, searchSuggestion, searchQuery} = action.params;
-              let engineStr = " - " +
+              let engineStr =
                 this._stringBundle.formatStringFromName("searchWithEngine",
                                                         [engineName], 1);
+              this._setUpDescription(this._actionText, engineStr, true);
 
               // Make the title by generating an array of pairs and its
               // corresponding interpolation string (e.g., "%1$S") to pass to
               // _generateEmphasisPairs.
               let pairs;
               if (searchSuggestion) {
                 // Check if the search query appears in the suggestion.  It may
                 // not.  If it does, then emphasize the query in the suggestion
@@ -1765,164 +1886,182 @@ extends="chrome://global/content/binding
                     [searchSuggestion, ""],
                   ];
                 }
               } else {
                 pairs = [
                   [searchQuery, ""],
                 ];
               }
-              pairs.push([engineStr, "selected"]);
               let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join("");
               title = this._generateEmphasisPairs(interpStr, pairs);
 
               // If this is a default search match, we remove the image so we
               // can style it ourselves with a generic search icon.
               // We don't do this when matching an aliased search engine,
               // because the icon helps with recognising which engine will be
               // used (when using the default engine, we don't need that
               // recognition).
-              if (!action.params.alias) {
+              if (!action.params.alias && !initialTypes.has("favicon")) {
                 this.removeAttribute("image");
               }
             } else if (action.type == "visiturl") {
               emphasiseUrl = false;
               displayUrl = this._unescapeUrl(action.params.url);
-              let sourceStr = this._stringBundle.GetStringFromName("visitURL");
-              title = this._generateEmphasisPairs(sourceStr, [
-                                                    [displayUrl, "match"],
-                                                  ]);
+              title = displayUrl;
+              let visitStr = this._stringBundle.GetStringFromName("visit");
+              this._setUpDescription(this._actionText, visitStr, true);
             }
           }
 
-          // Check if we have a search engine name
-          if (initialTypes.has("search")) {
-            emphasiseUrl = false;
-
-            const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 ";
-
-            let searchEngine = "";
-            [title, searchEngine] = title.split(TITLE_SEARCH_ENGINE_SEPARATOR);
-            displayUrl = this._stringBundle.formatStringFromName("searchWithEngine", [searchEngine], 1);
-          }
-
           if (!displayUrl) {
             let input = this.parentNode.parentNode.input;
             let url = typeof(input.trimValue) == "function" ?
                       input.trimValue(originalUrl) :
                       originalUrl;
             displayUrl = this._unescapeUrl(url);
           }
           this.setAttribute("displayurl", displayUrl);
 
-          // Check if we have an auto-fill URL
-          if (initialTypes.has("autofill")) {
-            emphasiseUrl = false;
-
-            let sourceStr = this._stringBundle.GetStringFromName("visitURL");
-            title = this._generateEmphasisPairs(sourceStr, [
-                                                 [displayUrl, "match"],
-                                                ]);
-          }
-
-          // If we have a tag match, show the tags and icon
-          if (type == "tag" || type == "bookmark-tag") {
-            // Configure the extra box for tags display
-            this._extraBox.hidden = false;
-            this._extraBox.childNodes[0].hidden = false;
-            this._extraBox.childNodes[1].hidden = true;
-            this._extraBox.pack = "end";
-            this._titleBox.flex = 1;
-
-            // The title is separated from the tags by an endash
-            let tags;
-            [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
-
-            // Each tag is split by a comma in an undefined order, so sort it
-            let sortedTags = tags.split(",").sort().join(", ");
-
-            // Emphasize the matching text in the tags
-            this._setUpDescription(this._extra, sortedTags);
-
-            // If we're suggesting bookmarks, then treat tagged matches as
-            // bookmarks for the star.
-            if (type == "bookmark-tag") {
-              type = "bookmark";
-            } else {
-              this._typeImage.hidden = true;
-            }
-          // keyword and favicon type results for search engines
-          // have an extra magnifying glass icon after them
-          } else if (type == "keyword" || (initialTypes.has("search") &&
-              initialTypes.has("favicon"))) {
-            // Configure the extra box for keyword display
-            this._extraBox.hidden = false;
-            this._extraBox.childNodes[0].hidden = true;
-            // The second child node is ":" and it should be hidden for non keyword types
-            this._extraBox.childNodes[1].hidden = type == "keyword" ? false : true;
-            this._extraBox.pack = "start";
-            this._titleBox.flex = 0;
-
-            // Hide the ellipsis so it doesn't take up space.
-            this._titleOverflowEllipsis.hidden = true;
-
-            if (type == "keyword") {
-              // Put the parameters next to the title if we have any
-              let search = this.getAttribute("text");
-              let params = "";
-              let paramsIndex = search.indexOf(" ");
-              if (paramsIndex != -1)
-                params = search.substr(paramsIndex + 1);
-
-              // Emphasize the keyword parameters
-              this._setUpDescription(this._extra, params);
-
-              // Don't emphasize keyword searches in the title or url
-              emphasiseUrl = false;
-              emphasiseTitle = false;
-            } else {
-              // Don't show any description for non keyword types.
-              this._setUpDescription(this._extra, "", true);
-            }
-            // If the result has the type favicon and a known search provider,
-            // customize it the same way as a keyword result.
-            type = "keyword";
-          }
-
-          // Give the image the icon style and a special one for the type
-          this._typeImage.className = "ac-type-icon" +
-            (type ? " ac-result-type-" + type : "");
-
           // Show the domain as the title if we don't have a title.
-          if (title == "") {
+          if (!title) {
             title = displayUrl;
             try {
               let uri = Services.io.newURI(originalUrl, null, null);
               // Not all valid URLs have a domain.
               if (uri.host)
                 title = uri.host;
             } catch (e) {}
           }
 
-          // Emphasize the matching search terms for the description
-          if (Array.isArray(title))
-            this._setUpEmphasisedSections(this._title, title);
-          else
-            this._setUpDescription(this._title, title, !emphasiseTitle);
+          this._tags.setAttribute("empty", "true");
+
+          if (type == "tag" || type == "bookmark-tag") {
+            // The title is separated from the tags by an endash
+            let tags;
+            [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
+
+            // Each tag is split by a comma in an undefined order, so sort it
+            let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => {
+              return a.localeCompare(a);
+            });
 
-          this._setUpDescription(this._url, displayUrl, !emphasiseUrl);
+            let anyTagsMatch = this._setUpTags(sortedTags);
+            if (anyTagsMatch) {
+              this._tags.removeAttribute("empty");
+            }
+            if (type == "bookmark-tag") {
+              type = "bookmark";
+            }
+          } else if (type == "keyword") {
+            // Note that this is a moz-action with action.type == keyword.
+            emphasiseUrl = false;
+            let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, "");
+            if (!keywordArg) {
+              // Treat keyword searches without arguments as visiturl actions.
+              type = "visiturl";
+              this.setAttribute("actiontype", "visiturl");
+              let visitStr = this._stringBundle.GetStringFromName("visit");
+              this._setUpDescription(this._actionText, visitStr, true);
+            } else {
+              let pairs = [[title, ""], [keywordArg, "match"]];
+              let interpStr =
+                this._stringBundle.GetStringFromName("bookmarkKeywordSearch");
+              title = this._generateEmphasisPairs(interpStr, pairs);
+              // The action box will be visible since this is a moz-action, but
+              // we want it to appear as if it were not visible, so set its text
+              // to the empty string.
+              this._setUpDescription(this._actionText, "", false);
+            }
+          }
 
-          // Set up overflow on a timeout because the contents of the box
-          // might not have a width yet even though we just changed them
-          setTimeout(this._setUpOverflow, 0, this._titleBox, this._titleOverflowEllipsis);
-          setTimeout(this._setUpOverflow, 0, this._urlBox, this._urlOverflowEllipsis);
+          this._typeIcon.setAttribute("type", type);
+          this._siteIcon.setAttribute("type", type);
+
+          if (Array.isArray(title)) {
+            this._setUpEmphasisedSections(this._titleText, title);
+          } else {
+            this._setUpDescription(this._titleText, title, false);
+          }
+          this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
+
+          if (this._inOverflow) {
+            this._handleOverflow();
+          }
           ]]>
         </body>
       </method>
 
+      <!-- This method truncates the displayed strings as necessary. -->
+      <method name="_handleOverflow">
+        <body><![CDATA[
+          let titleRect = this._titleText.getBoundingClientRect();
+          let tagsRect = this._tagsText.getBoundingClientRect();
+          let urlRect = this._urlText.getBoundingClientRect();
+          let actionRect = this._actionText.getBoundingClientRect();
+          let urlActionWidth = Math.max(urlRect.width, actionRect.width);
+
+          // Total width for the title and URL/action is the width of the item
+          // minus the start of the title text minus a little extra padding.
+          // This extra padding amount is basically arbitrary but balances out
+          // the listbox's padding on the left side.
+          let extraPadding = 30;
+          let itemWidth =
+            this.parentNode.getBoundingClientRect().width -
+            this._titleText.getBoundingClientRect().left -
+            extraPadding;
+
+          if (this._tags.hasAttribute("empty")) {
+            // The tags box is not displayed in this case.
+            tagsRect.width = 0;
+          }
+
+          let titleTagsWidth = titleRect.width + tagsRect.width;
+          if (titleTagsWidth + urlActionWidth > itemWidth) {
+            // Title + tags + URL/action overflows the item width.
+
+            // The percentage of the item width allocated to the title and tags.
+            let titleTagsPct = 0.66;
+
+            let titleTagsAvailable = itemWidth - urlActionWidth;
+            let titleTagsMaxWidth = Math.max(
+              titleTagsAvailable,
+              itemWidth * titleTagsPct
+            );
+            if (titleTagsWidth > titleTagsMaxWidth) {
+              // Title + tags overflows the max title + tags width.
+
+              // The percentage of the title + tags width allocated to the
+              // title.
+              let titlePct = 0.33;
+
+              let titleAvailable = titleTagsMaxWidth - tagsRect.width;
+              let titleMaxWidth = Math.max(
+                titleAvailable,
+                titleTagsMaxWidth * titlePct
+              );
+              let tagsAvailable = titleTagsMaxWidth - titleRect.width;
+              let tagsMaxWidth = Math.max(
+                tagsAvailable,
+                titleTagsMaxWidth * (1 - titlePct)
+              );
+              this._titleText.style.maxWidth = titleMaxWidth + "px";
+              this._tagsText.style.maxWidth = tagsMaxWidth + "px";
+            }
+            let urlActionAvailable = itemWidth - titleTagsWidth;
+            let urlActionMaxWidth = Math.max(
+              urlActionAvailable,
+              itemWidth * (1 - titleTagsPct)
+            );
+            this._urlText.style.maxWidth = urlActionMaxWidth + "px";
+            this._actionText.style.maxWidth = urlActionMaxWidth + "px";
+          }
+        ]]></body>
+      </method>
+
       <method name="_parseActionUrl">
         <parameter name="aUrl"/>
         <body><![CDATA[
           if (!aUrl.startsWith("moz-action:"))
             return null;
 
           // URL is in the format moz-action:ACTION,PARAMS
           // Where PARAMS is a JSON encoded object.
@@ -1945,78 +2084,16 @@ extends="chrome://global/content/binding
             action.params = {
               url: params,
             }
           }
 
           return action;
         ]]></body>
       </method>
-
-      <method name="_setUpOverflow">
-        <parameter name="aParentBox"/>
-        <parameter name="aEllipsis"/>
-        <body>
-          <![CDATA[
-          // Hide the ellipsis incase there's just enough to not underflow
-          aEllipsis.style.visibility = "hidden";
-
-          // Start with the parent's width and subtract off its children
-          let tooltip = [];
-          let children = aParentBox.childNodes;
-          let widthDiff = aParentBox.boxObject.width;
-
-          for (let i = 0; i < children.length; i++) {
-            // Only consider a child if it actually takes up space
-            let childWidth = children[i].boxObject.width;
-            if (childWidth > 0) {
-              // Subtract a little less to account for subpixel rounding
-              widthDiff -= childWidth - .5;
-
-              // Add to the tooltip if it's not hidden and has text
-              let childText = children[i].textContent;
-              if (childText)
-                tooltip.push(childText);
-            }
-          }
-
-          // If the children take up more space than the parent.. overflow!
-          if (widthDiff < 0) {
-            // Re-show the ellipsis now that we know it's needed
-            aEllipsis.style.visibility = "visible";
-
-            // Separate text components with a ndash --
-            aParentBox.tooltipText = tooltip.join(" \u2013 ");
-          }
-          ]]>
-        </body>
-      </method>
-
-      <method name="_doUnderflow">
-        <parameter name="aName"/>
-        <body>
-          <![CDATA[
-          // Hide the ellipsis right when we know we're underflowing instead of
-          // waiting for the timeout to trigger the _setUpOverflow calculations
-          this[aName + "Box"].tooltipText = "";
-          this[aName + "OverflowEllipsis"].style.visibility = "hidden";
-          ]]>
-        </body>
-      </method>
-
-      <method name="_doOverflow">
-        <parameter name="aName"/>
-        <body>
-          <![CDATA[
-          this._setUpOverflow(this[aName + "Box"],
-                              this[aName + "OverflowEllipsis"]);
-          ]]>
-        </body>
-      </method>
-
     </implementation>
   </binding>
 
   <binding id="autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree">
     <content>
       <children includes="treecols"/>
       <xul:treerows class="autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1">
         <children/>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -887,25 +887,16 @@ panel[type="autocomplete-richlistbox"] {
   display: none;
 }
 
 .autocomplete-history-dropmarker[enablehistory="true"] {
   display: -moz-box;
   -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#history-dropmarker");
 }
 
-.ac-ellipsis-after {
-  visibility: hidden;
-}
-
-.ac-url-text[type~="action"],
-.ac-action-text:not([type~="action"]) {
-  visibility: collapse;
-}
-
 %endif
 
 
 
 /* the C++ implementation of widgets is too eager to make popups visible.
    this causes problems (bug 120155 and others), thus this workaround: */
 popup[type="autocomplete"][hidden="true"] {
   visibility: hidden;
--- a/toolkit/locales/en-US/chrome/global/autocomplete.properties
+++ b/toolkit/locales/en-US/chrome/global/autocomplete.properties
@@ -1,12 +1,23 @@
 # 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/.
 
 # LOCALIZATION NOTE (searchWithEngine): %S will be replaced with
 # the search engine provider's name. This format was chosen because
 # the provider can also end with "Search" (e.g.: MSN Search).
 searchWithEngine = Search with %S
-switchToTab = Switch to tab
-# LOCALIZATION NOTE (visitURL):
-# %S is the URL to visit.
-visitURL = Visit %S
+
+# LOCALIZATION NOTE (switchToTab2): This is the same as the older switchToTab
+# string that it's replacing, except it uses title case, so "Switch" and "Tab"
+# are capitalized.
+switchToTab2 = Switch to Tab
+
+# LOCALIZATION NOTE (visit): This is shown next to autocomplete entries that are
+# simple URLs or sites, which will be visited when the user selects them.
+visit = Visit
+
+# LOCALIZATION NOTE (bookmarkKeywordSearch): This is the title of autocomplete
+# entries that are bookmark keyword searches.  %1$S will be replaced with the
+# domain name of the bookmark, and %2$S will be replaced with the keyword
+# search text that the user is typing.  %2$S will not be empty.
+bookmarkKeywordSearch = %1$S: %2$S
--- a/toolkit/themes/linux/global/autocomplete.css
+++ b/toolkit/themes/linux/global/autocomplete.css
@@ -102,106 +102,76 @@ treechildren.autocomplete-treebody::-moz
 /* ::::: richlistbox autocomplete ::::: */
 
 .autocomplete-richlistbox {
   -moz-appearance: none;
   margin: 1px;
   background-color: transparent;
 }
 
-.autocomplete-richlistitem[selected="true"] {
-  background-color: Highlight;
-  color: HighlightText;
-}
-
 .autocomplete-richlistitem {
-  padding: 6px 2px;
   color: MenuText;
 }
 
-.ac-url-box {
-  /* When setting a vertical margin here, half of that needs to be added
-     .ac-title-box's translateY for when .ac-url-box is hidden (see below). */
-  margin-top: 1px;
-}
-
-.autocomplete-richlistitem[actiontype="keyword"] .ac-url-box,
-.autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box,
-.autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box,
-.autocomplete-richlistitem[type~="autofill"] .ac-url-box {
-  visibility: hidden;
-}
-
-.autocomplete-richlistitem[actiontype="keyword"] .ac-title-box,
-.autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box,
-.autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box,
-.autocomplete-richlistitem[type~="autofill"] .ac-title-box {
-  /* Center the title by moving it down by half of .ac-url-box's height,
-     including vertical margins (if any). */
-  transform: translateY(.5em);
-}
-
-.ac-site-icon {
-  width: 16px; 
-  height: 16px;
-  margin-bottom: -2px;
-  -moz-margin-start: 3px;
-  -moz-margin-end: 6px;
+.autocomplete-richlistitem[selected] {
+  color: HighlightText;
 }
 
 .ac-type-icon {
-  width: 16px; 
+  width: 16px;
+  height: 16px;
+  max-width: 16px;
+  max-height: 16px;
+  -moz-margin-start: 13px;
+  -moz-margin-end: 6px;
+}
+
+.ac-site-icon {
+  width: 16px;
   height: 16px;
-  -moz-margin-start: 6px;
+  max-width: 16px;
+  max-height: 16px;
+  -moz-margin-start: 0;
+  -moz-margin-end: 11px;
+}
+
+.ac-title {
+  -moz-margin-start: 0;
+  -moz-margin-end: 6px;
+}
+
+html|span.ac-tag {
+  -moz-margin-start: 0;
+  -moz-margin-end: 2px;
+}
+
+.ac-tags {
+  -moz-margin-start: 0;
   -moz-margin-end: 4px;
 }
 
-.ac-extra > .ac-result-type-tag {
-  margin: 0 4px;
-}
-
-.ac-extra > .ac-comment {
-  padding-right: 4px;
-}
-
-.ac-ellipsis-after {
-  margin: 0 !important;
-  padding: 0; 
-  min-width: 1em;
-}
-
-.ac-normal-text {
-  margin: 0 !important;
-  padding: 0;
+html|span.ac-title-urlaction-separator {
+  padding-left: 0;
+  padding-right: 6px;
 }
 
-.ac-normal-text > html|span {
-  margin: 0 !important;
-  padding: 0;
-}
-
-html|span.ac-emphasize-text {
-  box-shadow: inset 0 0 1px 1px rgba(0,0,0,0.1);
-  background-color: rgba(0,0,0,0.05);
-  border-radius: 2px;
-  text-shadow: 0 0 currentColor; /*faux bold effect*/
+/* Better align the URL/action with the title. */
+.ac-tags,
+.ac-url,
+.ac-action {
+  margin-bottom: -2px;
 }
 
-.ac-url-text > html|span.ac-emphasize-text,
-.ac-action-text > html|span.ac-emphasize-text {
-  box-shadow: none;
-}
-
-.ac-normal-text[selected="true"] > html|span.ac-emphasize-text {
-  box-shadow: inset 0 0 1px 1px rgba(255,255,255,0.3);
-  background-color: rgba(255,255,255,0.2);
-}
-
-.ac-title, .ac-url {
-  overflow: hidden;
+.ac-title-text,
+.ac-tags-text,
+.ac-url-text,
+.ac-action-text,
+.ac-text-overflow-container {
+  padding: 0 !important;
+  margin: 0 !important;
 }
 
 /* ::::: textboxes inside toolbarpaletteitems ::::: */
 
 toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
--- a/toolkit/themes/osx/global/autocomplete.css
+++ b/toolkit/themes/osx/global/autocomplete.css
@@ -88,108 +88,68 @@ treechildren.autocomplete-treebody::-moz
 
 /* ::::: richlistbox autocomplete ::::: */
 
 .autocomplete-richlistbox {
   -moz-appearance: none;
   margin: 0;
 }
 
-.autocomplete-richlistitem[selected="true"] {
-  background-color: Highlight;
-  color: HighlightText;
-  background-image: linear-gradient(rgba(255,255,255,0.3), transparent);
-}
-
-.autocomplete-richlistitem {
-  padding: 5px 2px;
-}
-
-.ac-url-box {
-  /* When setting a vertical margin here, half of that needs to be added
-     .ac-title-box's translateY for when .ac-url-box is hidden (see below). */
-  margin-top: 1px;
-}
-
-.autocomplete-richlistitem[actiontype="keyword"] .ac-url-box,
-.autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box,
-.autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box,
-.autocomplete-richlistitem[type~="autofill"] .ac-url-box {
-  visibility: hidden;
-}
-
-.autocomplete-richlistitem[actiontype="keyword"] .ac-title-box,
-.autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box,
-.autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box,
-.autocomplete-richlistitem[type~="autofill"] .ac-title-box {
-  /* Center the title by moving it down by half of .ac-url-box's height,
-     including vertical margins (if any). */
-  transform: translateY(.5em);
-}
-
-.ac-site-icon {
-  width: 16px; 
-  height: 16px;
-  margin-bottom: -1px;
-  -moz-margin-start: 7px;
-  -moz-margin-end: 5px;
-}
-
 .ac-type-icon {
   width: 16px;
   height: 16px;
-  -moz-margin-start: 6px;
+  max-width: 16px;
+  max-height: 16px;
+  -moz-margin-start: 16px;
+  -moz-margin-end: 6px;
+}
+
+.ac-site-icon {
+  width: 16px;
+  height: 16px;
+  max-width: 16px;
+  max-height: 16px;
+  -moz-margin-start: 0;
+  -moz-margin-end: 11px;
+}
+
+.ac-title {
+  -moz-margin-start: 0;
+  -moz-margin-end: 6px;
+}
+
+html|span.ac-tag {
+  -moz-margin-start: 0;
+  -moz-margin-end: 2px;
+}
+
+.ac-tags {
+  -moz-margin-start: 0;
   -moz-margin-end: 4px;
 }
 
-.ac-url-box > .ac-site-icon,
-.ac-url-box > .ac-type-icon {
-  /* Otherwise the spacer is big enough to stretch its container */
-  height: auto;
-}
-
-.ac-extra > .ac-result-type-tag {
-  margin: 0 4px;
-}
-
-.ac-extra > .ac-comment {
-  padding-right: 4px;
-}
-
-.ac-ellipsis-after {
-  margin: 0 !important;
-  padding: 0; 
-  min-width: 1.1em;
+html|span.ac-title-urlaction-separator {
+  -moz-margin-start: 0;
+  -moz-margin-end: 6px;
 }
 
-.ac-normal-text {
-  margin: 0 !important;
-  padding: 0;
-}
-
-.ac-normal-text > html|span {
-  margin: 0 !important;
-  padding: 0;
+/* Better align the URL/action with the title. */
+.ac-tags,
+.ac-url,
+.ac-action {
+  margin-bottom: -2px;
 }
 
-html|span.ac-emphasize-text {
-  box-shadow: inset 0 0 1px 1px rgba(208,208,208,0.4);
-  background-color: rgba(208,208,208,0.2);
-  border-radius: 2px;
-  text-shadow: 0 0 currentColor;
-}
-
-.ac-url-text > html|span.ac-emphasize-text,
-.ac-action-text > html|span.ac-emphasize-text {
-  box-shadow: inset 0 0 1px 1px rgba(183,210,226,0.4);
-  background-color: rgba(183,210,226,0.3);
-}
-
-.ac-title, .ac-url {
-  overflow: hidden;
+.ac-title-text,
+.ac-tags-text,
+.ac-url-text,
+.ac-action-text,
+.ac-text-overflow-container {
+  padding: 0 !important;
+  margin: 0 !important;
 }
 
 /* ::::: textboxes inside toolbarpaletteitems ::::: */
 
 toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
--- a/toolkit/themes/windows/global/autocomplete.css
+++ b/toolkit/themes/windows/global/autocomplete.css
@@ -92,143 +92,68 @@ treechildren.autocomplete-treebody::-moz
 
 /* ::::: richlistbox autocomplete ::::: */
 
 .autocomplete-richlistbox {
   -moz-appearance: none;
   margin: 0;
 }
 
-.autocomplete-richlistitem {
-  padding: 1px;
-}
-
-.autocomplete-richlistitem[selected="true"] {
-  background-color: Highlight;
-  color: HighlightText;
-}
-
-%ifdef XP_WIN
-@media (-moz-os-version: windows-vista) and (-moz-windows-default-theme),
-       (-moz-os-version: windows-win7) and (-moz-windows-default-theme) {
-  .autocomplete-richlistitem[selected="true"] {
-    color: inherit;
-    background-color: transparent;
-    /* four gradients for the bevel highlights on each edge, one for blue background */
-    background-image:
-      linear-gradient(to bottom, rgba(255,255,255,0.9) 3px, transparent 3px),
-      linear-gradient(to right, rgba(255,255,255,0.5) 3px, transparent 3px),
-      linear-gradient(to left, rgba(255,255,255,0.5) 3px, transparent 3px),
-      linear-gradient(to top, rgba(255,255,255,0.4) 3px, transparent 3px),
-      linear-gradient(to bottom, rgba(163,196,247,0.3), rgba(122,180,246,0.3));
-    background-clip: content-box;
-    border-radius: 6px;
-    outline: 1px solid rgb(124,163,206);
-    -moz-outline-radius: 3px;
-    outline-offset: -2px;
-  }
-}
-%endif
-
-.ac-title-box {
-  margin-top: 4px;
-}
-
-.ac-url-box {
-  /* When setting a vertical margin here, half of that needs to be added
-     .ac-title-box's translateY for when .ac-url-box is hidden (see below). */
-  margin: 1px 0 4px;
-}
-
-.autocomplete-richlistitem[actiontype="keyword"] .ac-url-box,
-.autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box,
-.autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box,
-.autocomplete-richlistitem[type~="autofill"] .ac-url-box {
-  visibility: hidden;
-}
-
-.autocomplete-richlistitem[actiontype="keyword"] .ac-title-box,
-.autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box,
-.autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box,
-.autocomplete-richlistitem[type~="autofill"] .ac-title-box {
-  /* Center the title by moving it down by half of .ac-url-box's height,
-     including vertical margins (if any). */
-  transform: translateY(calc(.5em + 2px));
+.ac-type-icon {
+  width: 16px;
+  height: 16px;
+  max-width: 16px;
+  max-height: 16px;
+  -moz-margin-start: 14px;
+  -moz-margin-end: 6px;
 }
 
 .ac-site-icon {
-  width: 16px; 
+  width: 16px;
   height: 16px;
-  margin: 0 5px -2px;
-}
-
-.ac-type-icon {
-  width: 16px; 
-  height: 16px;
-  -moz-margin-start: 6px;
-  -moz-margin-end: 4px;
-  margin-bottom: -1px;
+  max-width: 16px;
+  max-height: 16px;
+  -moz-margin-start: 0;
+  -moz-margin-end: 11px;
 }
 
-.ac-url-box > .ac-site-icon,
-.ac-url-box > .ac-type-icon {
-  /* Otherwise the spacer is big enough to stretch its container */
-  height: auto;
+.ac-title {
+  -moz-margin-start: 0;
+  -moz-margin-end: 6px;
 }
 
-.ac-extra > .ac-result-type-tag {
-  margin: 0 4px;
-}
-
-.ac-extra > .ac-comment {
-  padding-right: 4px;
-}
-
-.ac-ellipsis-after {
-  margin: 0 !important;
-  padding: 0; 
-  min-width: 1em;
+html|span.ac-tag {
+  -moz-margin-start: 0;
+  -moz-margin-end: 2px;
 }
 
-.ac-normal-text {
-  margin: 0 !important;
-  padding: 0;
+.ac-tags {
+  -moz-margin-start: 0;
+  -moz-margin-end: 4px;
 }
 
-.ac-normal-text > html|span {
-  margin: 0 !important;
-  padding: 0;
-}
-
-html|span.ac-emphasize-text {
-  box-shadow: inset 0 0 1px 1px rgba(208,208,208,0.5);
-  background-color: rgba(208,208,208,0.3);
-  border-radius: 2px;
-  text-shadow: 0 0 currentColor;
+html|span.ac-title-urlaction-separator {
+  padding-left: 0;
+  padding-right: 6px;
 }
 
-@media (-moz-windows-default-theme) {
-  @media not all and (-moz-os-version: windows-xp) {
-    html|span.ac-emphasize-text {
-      box-shadow: inset 0 0 1px 1px rgba(0,0,0,0.1);
-      background-color: rgba(0,0,0,0.05);
-    }
-  }
-
-  @media (-moz-os-version: windows-xp) {
-    .ac-url-text > html|span.ac-emphasize-text,
-    .ac-action-text > html|span.ac-emphasize-text {
-      box-shadow: inset 0 0 1px 1px rgba(202,214,201,0.3);
-      background-color: rgba(202,214,201,0.2);
-    }
-  }
+/* Better align the URL/action with the title. */
+.ac-tags,
+.ac-url,
+.ac-action {
+  margin-bottom: -2px;
 }
 
-.ac-title, .ac-url {
-  overflow: hidden;
+.ac-title-text,
+.ac-tags-text,
+.ac-url-text,
+.ac-action-text,
+.ac-text-overflow-container {
+  padding: 0 !important;
+  margin: 0 !important;
 }
 
 /* ::::: textboxes inside toolbarpaletteitems ::::: */
 
 toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
--- a/toolkit/themes/windows/global/jar.mn
+++ b/toolkit/themes/windows/global/jar.mn
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 #include ../../shared/non-mac.jar.inc.mn
 
 toolkit.jar:
-* skin/classic/global/autocomplete.css
+  skin/classic/global/autocomplete.css
 #ifndef MOZ_THEME_FASTSTRIPE
   skin/classic/global/button.css
   skin/classic/global/checkbox.css
   skin/classic/global/dropmarker.css
   skin/classic/global/groupbox.css
 * skin/classic/global/menu.css
   skin/classic/global/menulist.css
 * skin/classic/global/popup.css