merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 06 Apr 2016 11:59:25 +0200
changeset 291871 68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8
parent 291834 05ce1f2670ae52e928af76df72e78989935141e3 (current diff)
parent 291870 75bef7c5fd91e42e5cfa5337f02f48942b568996 (diff)
child 291872 49d808d13f4fab71b49019905cce3ba6eb6bb31f
child 292094 e1a8f719e764e35db6b364213d3efc1d3890e81e
push id74700
push usercbook@mozilla.com
push dateWed, 06 Apr 2016 10:06:38 +0000
treeherdermozilla-inbound@49d808d13f4f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
first release with
nightly linux32
68c0b7d6f16c / 48.0a1 / 20160406030221 / files
nightly linux64
68c0b7d6f16c / 48.0a1 / 20160406030221 / files
nightly mac
68c0b7d6f16c / 48.0a1 / 20160406030221 / files
nightly win32
68c0b7d6f16c / 48.0a1 / 20160406030221 / files
nightly win64
68c0b7d6f16c / 48.0a1 / 20160406030221 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
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