Bug 1330315 - Add a telemetry probe to track how the Preferences are opened. r?jaws draft
authorAvalon <lzylong@gmail.com>
Wed, 26 Apr 2017 15:44:42 -0400
changeset 568945 eb4fe512fe6e6a6372b9b6b950402159f911e631
parent 555608 81e37ef1360ba4505726ddf542ebdcc952a57578
child 626072 3f48b1a644fa414519c17d9510aadf58bc51e36c
push id56029
push userbmo:lzylong@gmail.com
push dateWed, 26 Apr 2017 19:46:17 +0000
reviewersjaws
bugs1330315
milestone55.0a1
Bug 1330315 - Add a telemetry probe to track how the Preferences are opened. r?jaws MozReview-Commit-ID: GmPBscfb2VF
browser/base/content/baseMenuOverlay.xul
browser/base/content/browser-data-submission-info-bar.js
browser/base/content/browser-fxaccounts.js
browser/base/content/browser-media.js
browser/base/content/browser-menubar.inc
browser/base/content/browser-sets.inc
browser/base/content/browser-syncui.js
browser/base/content/browser.js
browser/base/content/utilityOverlay.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/newtab/NewTabSearchProvider.jsm
browser/components/nsBrowserContentHandler.js
browser/components/nsBrowserGlue.js
browser/components/search/content/search.xml
browser/components/translation/translation-infobar.xml
browser/components/uitour/UITour.jsm
browser/modules/AboutHome.jsm
browser/modules/ContentSearch.jsm
toolkit/components/telemetry/Histograms.json
toolkit/content/aboutTelemetry.js
toolkit/mozapps/extensions/content/extensions.js
--- a/browser/base/content/baseMenuOverlay.xul
+++ b/browser/base/content/baseMenuOverlay.xul
@@ -17,17 +17,17 @@
 <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
 
 #ifdef XP_MACOSX
 <!-- nsMenuBarX hides these and uses them to build the Application menu.
      When using Carbon widgets for Mac OS X widgets, some of these are not
      used as they only apply to Cocoa widget builds. All version of Firefox
      through Firefox 2 will use Carbon widgets. -->
     <menupopup id="menu_ToolsPopup">
-        <menuitem id="menu_preferences" label="&preferencesCmdMac.label;" key="key_preferencesCmdMac" oncommand="openPreferences();"/>
+        <menuitem id="menu_preferences" label="&preferencesCmdMac.label;" key="key_preferencesCmdMac" oncommand="openPreferences(undefined, {origin: 'commandLineLegacy'});"/>
         <menuitem id="menu_mac_services" label="&servicesMenuMac.label;"/>
         <menuitem id="menu_mac_hide_app" label="&hideThisAppCmdMac2.label;" key="key_hideThisAppCmdMac"/>
         <menuitem id="menu_mac_hide_others" label="&hideOtherAppsCmdMac.label;" key="key_hideOtherAppsCmdMac"/>
         <menuitem id="menu_mac_show_all" label="&showAllAppsCmdMac.label;"/>
     </menupopup>
 <!-- Mac window menu -->
 #include ../../../toolkit/content/macWindowMenu.inc
 #endif
--- a/browser/base/content/browser-data-submission-info-bar.js
+++ b/browser/base/content/browser-data-submission-info-bar.js
@@ -64,33 +64,33 @@ var gDataNotificationInfoBar = {
       label: gNavigatorBundle.getString("dataReportingNotification.button.label"),
       accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
       popup: null,
       callback: () => {
         this._actionTaken = true;
         // The advanced subpanes are only supported in the old organization, which will
         // be removed by bug 1349689.
         if (Preferences.get("browser.preferences.useOldOrganization", false)) {
-          window.openAdvancedPreferences("dataChoicesTab");
+          window.openAdvancedPreferences("dataChoicesTab", {origin:"dataReporting"});
         } else {
-          window.openPreferences("paneAdvanced");
+          window.openPreferences("paneAdvanced", {origin: "dataReporting"});
         }
       },
     }];
 
     this._log.info("Creating data reporting policy notification.");
     this._notificationBox.appendNotification(
       message,
       this._DATA_REPORTING_NOTIFICATION,
       null,
       this._notificationBox.PRIORITY_INFO_HIGH,
       buttons,
       event => {
         if (event == "removed") {
-          Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
+          Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close");
         }
       }
     );
     // It is important to defer calling onUserNotifyComplete() until we're
     // actually sure the notification was displayed. If we ever called
     // onUserNotifyComplete() without showing anything to the user, that
     // would be very good for user choice. It may also have legal impact.
     request.onUserNotifyComplete();
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -277,35 +277,35 @@ var gFxAccounts = {
       updateWithUserData(null);
     });
   },
 
   onMenuPanelCommand() {
 
     switch (this.panelUIFooter.getAttribute("fxastatus")) {
     case "signedin":
-      this.openPreferences();
+      this.openPreferences("fxaSignedin");
       break;
     case "error":
       if (this.panelUIFooter.getAttribute("unverified")) {
-        this.openPreferences();
+        this.openPreferences("fxaError");
       } else {
         this.openSignInAgainPage("menupanel");
       }
       break;
     default:
-      this.openPreferences();
+      this.openPreferences("fxa");
       break;
     }
 
     PanelUI.hide();
   },
 
-  openPreferences() {
-    openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } });
+  openPreferences(origin) {
+    openPreferences("paneSync", { origin, urlParams: { entrypoint: "menupanel" } });
   },
 
   openAccountsPage(action, urlParams = {}) {
     let params = new URLSearchParams();
     if (action) {
       params.set("action", action);
     }
     for (let name in urlParams) {
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -162,17 +162,17 @@ var gEMEHandler = {
       Services.prefs.setBoolPref(firstPlayPref, true);
     } else {
       document.getElementById(anchorId).removeAttribute("firstplay");
     }
 
     let mainAction = {
       label: gNavigatorBundle.getString(btnLabelId),
       accessKey: gNavigatorBundle.getString(btnAccessKeyId),
-      callback() { openPreferences("panePrivacy"); },
+      callback() { openPreferences("panePrivacy", {origin: "browserMedia"}); },
       dismiss: true
     };
     let options = {
       dismissed: true,
       eventCallback: aTopic => aTopic == "swapping",
       learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content",
     };
     PopupNotifications.show(browser, "drmContentPlaying", message, anchorId, mainAction, null, options);
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -180,17 +180,17 @@
                           accesskey="&bidiSwitchTextDirectionItem.accesskey;"
                           hidden="true"/>
 #ifdef XP_UNIX
 #ifndef XP_MACOSX
                 <menuseparator/>
                 <menuitem id="menu_preferences"
                           label="&preferencesCmdUnix.label;"
                           accesskey="&preferencesCmdUnix.accesskey;"
-                          oncommand="openPreferences();"/>
+                          oncommand="openPreferences(undefined, {origin: 'menubar'});"/>
 #endif
 #endif
               </menupopup>
             </menu>
 
             <menu id="view-menu" label="&viewMenu.label;"
                   accesskey="&viewMenu.accesskey;">
               <menupopup id="menu_viewPopup"
@@ -532,17 +532,17 @@
                 <menupopup id="menu_mirrorTab-popup"
                            onpopupshowing="populateMirrorTabMenu(this)"/>
               </menu>
 #ifndef XP_UNIX
               <menuseparator id="prefSep"/>
               <menuitem id="menu_preferences"
                         label="&preferencesCmd2.label;"
                         accesskey="&preferencesCmd2.accesskey;"
-                        oncommand="openPreferences();"/>
+                        oncommand="openPreferences(undefined, {origin: 'menubar'});"/>
 #endif
               </menupopup>
             </menu>
 
 #ifdef XP_MACOSX
           <menu id="windowMenu" />
 #endif
           <menu id="helpMenu" />
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -88,18 +88,17 @@
     <command id="cmd_fullZoomReset"   oncommand="FullZoom.reset()"/>
     <command id="cmd_fullZoomToggle"  oncommand="ZoomManager.toggleZoom();"/>
     <command id="cmd_gestureRotateLeft" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
     <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
     <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/>
     <command id="Browser:OpenLocation" oncommand="openLocation();"/>
     <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/>
     <command id="Browser:NewUserContextTab" oncommand="openNewUserContextTab(event.sourceEvent);"/>
-    <command id="Browser:OpenAboutContainers" oncommand="openPreferences('paneContainers');"/>
-
+    <command id="Browser:OpenAboutContainers" oncommand="openPreferences('paneContainers', {origin: 'ContainersCommand'});"/>
     <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
     <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
     <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
     <command id="Tools:Sanitize"
      oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/>
     <command id="Tools:PrivateBrowsing"
       oncommand="OpenBrowserWindow({private: true});"/>
 #ifdef E10S_TESTING_ONLY
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -259,17 +259,17 @@ var gSyncUI = {
 
   /**
    * Open the Sync preferences.
    *
    * @param entryPoint
    *        Indicates the entrypoint from where this method was called.
    */
   openPrefs(entryPoint = "syncbutton") {
-    openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
+    openPreferences("paneSync", { urlParams: { entrypoint: entryPoint }, origin: "paneSync" });
   },
 
   openSignInAgainPage(entryPoint = "syncbutton") {
     gFxAccounts.openSignInAgainPage(entryPoint);
   },
 
   openSyncedTabsPanel() {
     let placement = CustomizableUI.getPlacementOfWidget("sync-button");
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -503,19 +503,19 @@ const gStoragePressureObserver = {
       buttons.push({
         label: prefStrBundle.getString(prefButtonLabelStringID),
         accessKey: prefStrBundle.getString(prefButtonAccesskeyStringID),
         callback(notificationBar, button) {
           // The advanced subpanes are only supported in the old organization, which will
           // be removed by bug 1349689.
           let win = gBrowser.ownerGlobal;
           if (Preferences.get("browser.preferences.useOldOrganization", false)) {
-            win.openAdvancedPreferences("networkTab");
+            win.openAdvancedPreferences("networkTab", origin: "storageManager" });
           } else {
-            win.openPreferences("panePrivacy");
+            win.openPreferences("panePrivacy", origin: "storageManager" });
           }
         }
       });
     }
 
     notificationBox.appendNotification(
       msg, "storage-pressure-notification", null, notificationBox.PRIORITY_WARNING_HIGH, buttons, null);
   }
@@ -1573,16 +1573,19 @@ var gBrowserInit = {
       }
 
       // Enable the Restore Last Session command if needed
       RestoreLastSessionObserver.init();
 
       SidebarUI.startDelayedLoad();
       SocialUI.init();
 
+      // Start monitoring slow add-ons
+      AddonWatcher.init();
+
       // Telemetry for master-password - we do this after 5 seconds as it
       // can cause IO if NSS/PSM has not already initialized.
       setTimeout(() => {
         if (window.closed) {
           return;
         }
         let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
                         .getService(Ci.nsIPK11TokenDB);
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -706,16 +706,21 @@ function openAboutDialog() {
   } else {
     features += "centerscreen,dependent,dialog=no";
   }
 
   window.openDialog("chrome://browser/content/aboutDialog.xul", "", features);
 }
 
 function openPreferences(paneID, extraArgs) {
+  if (extraArgs.origin) {
+    Services.telemetry.getHistogramById("FX_PREFERENCES_OPENED_VIA").add(extraArgs.origin);
+  } else {
+    Services.telemetry.getHistogramById("FX_PREFERENCES_OPENED_VIA").add("other");
+  }
   function switchToAdvancedSubPane(doc) {
     if (extraArgs && extraArgs["advancedTab"]) {
       let advancedPaneTabs = doc.getElementById("advancedPrefs");
       advancedPaneTabs.selectedTab = doc.getElementById(extraArgs["advancedTab"]);
     }
   }
 
   // This function is duplicated from preferences.js.
@@ -771,18 +776,18 @@ function openPreferences(paneID, extraAr
   } else {
     if (paneID) {
       browser.contentWindow.gotoPref(paneID);
     }
     switchToAdvancedSubPane(browser.contentDocument);
   }
 }
 
-function openAdvancedPreferences(tabID) {
-  openPreferences("paneAdvanced", { "advancedTab": tabID });
+function openAdvancedPreferences(tabID, origin) {
+  openPreferences("paneAdvanced", { "advancedTab" : tabID, origin });
 }
 
 /**
  * Opens the troubleshooting information (about:support) page for this version
  * of the application.
  */
 function openTroubleshootingPage() {
   openUILinkIn("about:support", "tab");
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -1144,17 +1144,17 @@ const CustomizableWidgets = [
     ]),
   }];
 
 let preferencesButton = {
   id: "preferences-button",
   defaultArea: CustomizableUI.AREA_PANEL,
   onCommand(aEvent) {
     let win = aEvent.target.ownerGlobal;
-    win.openPreferences();
+    win.openPreferences(undefined, {origin: "preferencesButton"});
   }
 };
 if (AppConstants.platform == "win") {
   preferencesButton.label = "preferences-button.labelWin";
   preferencesButton.tooltiptext = "preferences-button.tooltipWin2";
 } else if (AppConstants.platform == "macosx") {
   preferencesButton.tooltiptext = "preferences-button.tooltiptext.withshortcut";
   preferencesButton.shortcutId = "key_preferencesCmdMac";
--- a/browser/components/newtab/NewTabSearchProvider.jsm
+++ b/browser/components/newtab/NewTabSearchProvider.jsm
@@ -67,17 +67,17 @@ SearchProvider.prototype = {
   },
 
   removeFormHistory({browser}, suggestion) {
     ContentSearch.removeFormHistoryEntry({target: browser}, suggestion);
   },
 
   manageEngines(browser) {
     const browserWin = browser.ownerGlobal;
-    browserWin.openPreferences("paneGeneral");
+    browserWin.openPreferences("paneSearch", { origin: "contentSearch" });
   },
 
   asyncGetState: Task.async(function*() {
     let state = yield ContentSearch.currentStateObj(true);
     return state;
   }),
 
   asyncPerformSearch: Task.async(function*({browser}, searchData) {
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -212,17 +212,22 @@ function openWindow(parent, url, target,
   argArray.appendElement(null, /* weak =*/ false); // charset
   argArray.appendElement(null, /* weak =*/ false); // referer
   argArray.appendElement(null, /* weak =*/ false); // postData
   argArray.appendElement(null, /* weak =*/ false); // allowThirdPartyFixup
 
   return Services.ww.openWindow(parent, url, target, features, argArray);
 }
 
-function openPreferences() {
+function openPreferences(extraArgs) {
+  if (extraArgs.origin) {
+    Services.telemetry.getHistogramById("FX_PREFERENCES_OPENED_VIA").add(extraArgs.origin);
+  } else {
+    Services.telemetry.getHistogramById("FX_PREFERENCES_OPENED_VIA").add("other");
+  }
   var args = Components.classes["@mozilla.org/array;1"]
                      .createInstance(Components.interfaces.nsIMutableArray);
 
   var wuri = Components.classes["@mozilla.org/supports-string;1"]
                        .createInstance(Components.interfaces.nsISupportsString);
   wuri.data = "about:preferences";
 
   args.appendElement(wuri, /* weak = */ false);
@@ -348,17 +353,17 @@ nsBrowserContentHandler.prototype = {
     }
 
     var chromeParam = cmdLine.handleFlagWithParam("chrome", false);
     if (chromeParam) {
 
       // Handle old preference dialog URLs.
       if (chromeParam == "chrome://browser/content/pref/pref.xul" ||
           chromeParam == "chrome://browser/content/preferences/preferences.xul") {
-        openPreferences();
+        openPreferences({origin: "commandLineLegacy"});
         cmdLine.preventDefault = true;
       } else try {
         let resolvedURI = resolveURIInternal(cmdLine, chromeParam);
         let isLocal = uri => {
           let localSchemes = new Set(["chrome", "file", "resource"]);
           if (uri instanceof Components.interfaces.nsINestedURI) {
             uri = uri.QueryInterface(Components.interfaces.nsINestedURI).innerMostURI;
           }
@@ -373,17 +378,17 @@ nsBrowserContentHandler.prototype = {
           dump("*** Preventing load of web URI as chrome\n");
           dump("    If you're trying to load a webpage, do not pass --chrome.\n");
         }
       } catch (e) {
         Components.utils.reportError(e);
       }
     }
     if (cmdLine.handleFlag("preferences", false)) {
-      openPreferences();
+      openPreferences({origin: "commandLine"});
       cmdLine.preventDefault = true;
     }
     if (cmdLine.handleFlag("silent", false))
       cmdLine.preventDefault = true;
 
     try {
       var privateWindowParam = cmdLine.handleFlagWithParam("private-window", false);
       if (privateWindowParam) {
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2077,16 +2077,17 @@ BrowserGlue.prototype = {
       Services.prefs.savePrefFile(null);
     }
   }),
 
   /**
    * Open preferences even if there are no open windows.
    */
   _openPreferences(...args) {
+    Services.telemetry.getHistogramById("FX_PREFERENCES_OPENED_VIA").add("nsIObserver");
     if (Services.appShell.hiddenDOMWindow.openPreferences) {
       Services.appShell.hiddenDOMWindow.openPreferences(...args);
       return;
     }
 
     let chromeWindow = RecentWindow.getMostRecentBrowserWindow();
     chromeWindow.openPreferences(...args);
   },
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -67,36 +67,42 @@
         </xul:hbox>
       </xul:textbox>
     </content>
 
     <implementation implements="nsIObserver">
       <constructor><![CDATA[
         if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
           return;
-        // Make sure we rebuild the popup in onpopupshowing
-        this._needToBuildPopup = true;
 
-        Services.obs.addObserver(this, "browser-search-engine-modified", false);
+        Services.obs.addObserver(this, "browser-search-engine-modified");
 
         this._initialized = true;
 
         Services.search.init((function search_init_cb(aStatus) {
           // Bail out if the binding's been destroyed
           if (!this._initialized)
             return;
 
           if (Components.isSuccessCode(aStatus)) {
             // Refresh the display (updating icon, etc)
             this.updateDisplay();
             BrowserSearch.updateOpenSearchBadge();
           } else {
             Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus);
           }
         }).bind(this));
+
+        // Some accessibility tests create their own <searchbar> that doesn't
+        // use the popup binding below, so null-check oneOffButtons.
+        if (this.textbox.popup.oneOffButtons) {
+          this.textbox.popup.oneOffButtons.telemetryOrigin = "searchbar";
+          this.textbox.popup.oneOffButtons.popup = this.textbox.popup;
+          this.textbox.popup.oneOffButtons.textbox = this.textbox;
+        }
       ]]></constructor>
 
       <destructor><![CDATA[
         this.destroy();
       ]]></destructor>
 
       <method name="destroy">
         <body><![CDATA[
@@ -373,16 +379,21 @@
         <parameter name="aParams"/>
         <body><![CDATA[
           var textBox = this._textbox;
           var textValue = textBox.value;
 
           let selection = this.telemetrySearchDetails;
           let oneOffRecorded = false;
 
+          BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
+            aEvent,
+            selection ? selection.index : -1
+          );
+
           if (!selection || (selection.index == -1)) {
             oneOffRecorded = this.textbox.popup.oneOffButtons
                                  .maybeRecordTelemetry(aEvent, aWhere, aParams);
             if (!oneOffRecorded) {
               let source = "unknown";
               let type = "unknown";
               let target = aEvent.originalTarget;
               if (aEvent instanceof KeyboardEvent) {
@@ -637,17 +648,17 @@
           if (navigator.platform.startsWith("Mac") && aEvent.keyCode == KeyEvent.VK_F4)
             this.openSearch()
         }, true);
 
         this.controllers.appendController(this.searchbarController);
         document.getBindingParent(this)._textboxInitialized = true;
 
         // Add observer for suggest preference
-        Services.prefs.addObserver("browser.search.suggest.enabled", this, false);
+        Services.prefs.addObserver("browser.search.suggest.enabled", this);
       ]]></constructor>
 
       <destructor><![CDATA[
         Services.prefs.removeObserver("browser.search.suggest.enabled", this);
 
         // Because XBL and the customize toolbar code interacts poorly,
         // there may not be anything to remove here
         try {
@@ -812,20 +823,18 @@
           if (!popup.popupOpen)
             return;
 
           // accel + up/down changes the default engine and shouldn't affect
           // the selection on the one-off buttons.
           if (aEvent.getModifierState("Accel"))
             return;
 
-          let suggestions =
-            document.getAnonymousElementByAttribute(popup, "anonid", "tree");
           let suggestionsHidden =
-            suggestions.getAttribute("collapsed") == "true";
+            popup.tree.getAttribute("collapsed") == "true";
           let numItems = suggestionsHidden ? 0 : this.popup.view.rowCount;
           this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
         ]]></body>
       </method>
 
       <!-- nsIController -->
       <field name="searchbarController" readonly="true"><![CDATA[({
         _self: this,
@@ -921,25 +930,21 @@
                    role="presentation"/>
       </xul:hbox>
       <xul:tree anonid="tree" flex="1"
                 class="autocomplete-tree plain search-panel-tree"
                 hidecolumnpicker="true" seltype="single">
         <xul:treecols anonid="treecols">
           <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
         </xul:treecols>
-        <xul:treechildren class="autocomplete-treebody"/>
+        <xul:treechildren class="autocomplete-treebody searchbar-treebody"/>
       </xul:tree>
       <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
     </content>
     <implementation>
-      <field name="AppConstants" readonly="true">
-        (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
-      </field>
-
       <method name="openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body><![CDATA[
           // initially the panel is hidden
           // to avoid impacting startup / new window performance
           aInput.popup.hidden = false;
 
@@ -970,25 +975,30 @@
           if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
               !aEvent.altKey && !aEvent.metaKey) {
             controller.handleEnter(true, aEvent);
             return;
           }
 
           // Check for middle-click or modified clicks on the search bar
           if (popupForSearchBar) {
+            BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
+              aEvent,
+              this.selectedIndex
+            );
+
             // Handle search bar popup clicks
             var search = controller.getValueAt(this.selectedIndex);
 
             // open the search results according to the clicking subtlety
             var where = whereToOpenLink(aEvent, false, true);
             let params = {};
 
             // But open ctrl/cmd clicks on autocomplete items in a new background tab.
-            let modifier = this.AppConstants.platform == "macosx" ?
+            let modifier = AppConstants.platform == "macosx" ?
                            aEvent.metaKey :
                            aEvent.ctrlKey;
             if (where == "tab" && (aEvent instanceof MouseEvent) &&
                 (aEvent.button == 1 || modifier))
               params.inBackground = true;
 
             // leave the popup open for background tab loads
             if (!(where == "tab" && params.inBackground)) {
@@ -1061,58 +1071,52 @@
           let searchbar = document.getElementById("searchbar");
           searchbar.handleSearchCommandWhere(event, engine, where, params);
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="popupshowing"><![CDATA[
-        if (!this.oneOffButtons.popup) {
-          // The panel width only spans to the textbox size, but we also want it
-          // to include the magnifier icon's width.
-          let ltr = getComputedStyle(this).direction == "ltr";
-          let magnifierWidth = parseInt(getComputedStyle(this)[
-                                 ltr ? "marginLeft" : "marginRight"
-                               ]) * -1;
-          // Ensure the panel is wide enough to fit at least 3 engines.
-          let minWidth = Math.max(
-            parseInt(this.width) + magnifierWidth,
-            this.oneOffButtons.buttonWidth * 3
-          );
-          this.style.minWidth = minWidth + "px";
+        // Force the panel to have the width of the searchbar rather than
+        // the width of the textfield.
+        let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDOMWindowUtils);
+        let textboxRect = DOMUtils.getBoundsWithoutFlushing(this.mInput);
+        let inputRect = DOMUtils.getBoundsWithoutFlushing(this.mInput.inputField);
 
-          // Set the origin before assigning the popup, as the assignment does
-          // a rebuild and would miss the origin.
-          this.oneOffButtons.telemetryOrigin = "searchbar";
-          // Set popup after setting the minWidth since it builds the buttons.
-          this.oneOffButtons.popup = this;
-          this.oneOffButtons.textbox = this.input;
-        }
+        // Ensure the panel is wide enough to fit at least 3 engines.
+        let minWidth = Math.max(textboxRect.width,
+                                this.oneOffButtons.buttonWidth * 3);
+        this.style.minWidth = minWidth + "px";
+        // Alignment of the panel with the searchbar is obtained with negative
+        // margins.
+        this.style.marginLeft = (textboxRect.left - inputRect.left) + "px";
+        // This second margin is needed when the direction is reversed,
+        // eg. when using command+shift+X.
+        this.style.marginRight = (inputRect.right - textboxRect.right) + "px";
 
         // First handle deciding if we are showing the reduced version of the
         // popup containing only the preferences button. We do this if the
         // glass icon has been clicked if the text field is empty.
         let searchbar = document.getElementById("searchbar");
-        let tree = document.getAnonymousElementByAttribute(this, "anonid",
-                                                           "tree")
         if (searchbar.hasAttribute("showonlysettings")) {
           searchbar.removeAttribute("showonlysettings");
           this.setAttribute("showonlysettings", "true");
 
           // Setting this with an xbl-inherited attribute gets overridden the
           // second time the user clicks the glass icon for some reason...
-          tree.collapsed = true;
+          this.tree.collapsed = true;
         } else {
           this.removeAttribute("showonlysettings");
           // Uncollapse as long as we have a tree with a view which has >= 1 row.
           // The autocomplete binding itself will take care of uncollapsing later,
           // if we currently have no rows but end up having some in the future
           // when the search string changes
-          tree.collapsed = !tree.view || !tree.view.rowCount;
+          this.tree.collapsed = !this.tree.view || !this.tree.view.rowCount;
         }
 
         // Show the current default engine in the top header of the panel.
         this.updateHeader();
       ]]></handler>
 
       <handler event="popuphiding"><![CDATA[
         this._isHiding = true;
@@ -1134,16 +1138,29 @@
           return;
         }
         this.oneOffButtons.handleSearchCommand(event, engine);
       ]]></handler>
     </handlers>
 
   </binding>
 
+
+  <!-- This is the same as the autocomplete-treebody binding except it does not
+       select rows on mousemove. -->
+  <binding id="searchbar-treebody"
+           extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-treebody">
+    <handlers>
+      <handler event="mousemove"><![CDATA[
+        // Cancel the event so that the base binding doesn't select the row.
+        event.preventDefault();
+      ]]></handler>
+    </handlers>
+  </binding>
+
   <!-- Used for additional open search providers in the search panel. -->
   <binding id="addengine-icon" extends="xul:box">
     <content>
       <xul:image class="addengine-icon" xbl:inherits="src"/>
       <xul:image class="addengine-badge"/>
     </content>
   </binding>
 
@@ -1184,53 +1201,49 @@
                        class="search-panel-one-offs"
                        xbl:inherits="compact">
         <xul:button anonid="search-settings-compact"
                     oncommand="showSettings();"
                     class="searchbar-engine-one-off-item search-setting-button-compact"
                     tooltiptext="&changeSearchSettings.tooltip;"
                     xbl:inherits="compact"/>
       </xul:description>
-      <xul:vbox anonid="add-engines"/>
+      <xul:vbox anonid="add-engines" class="search-add-engines"/>
       <xul:button anonid="search-settings"
                   oncommand="showSettings();"
                   class="search-setting-button search-panel-header"
                   label="&changeSearchSettings.button;"
                   xbl:inherits="compact"/>
       <xul:menupopup anonid="search-one-offs-context-menu">
         <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
                       label="&searchInNewTab.label;"
                       accesskey="&searchInNewTab.accesskey;"/>
         <xul:menuitem anonid="search-one-offs-context-set-default"
                       label="&searchSetAsDefault.label;"
                       accesskey="&searchSetAsDefault.accesskey;"/>
       </xul:menupopup>
     </content>
 
-    <implementation implements="nsIDOMEventListener">
+    <implementation implements="nsIDOMEventListener,nsIObserver,nsIWeakReference">
 
       <!-- Width in pixels of the one-off buttons.  49px is the min-width of
            each search engine button, adapt this const when changing the css.
            It's actually 48px + 1px of right border. -->
       <property name="buttonWidth" readonly="true" onget="return 49;"/>
 
       <field name="_popup">null</field>
 
       <!-- The popup that contains the one-offs.  This is required, so it should
            never be null or undefined, except possibly before the one-offs are
            used. -->
       <property name="popup">
         <getter><![CDATA[
           return this._popup;
         ]]></getter>
         <setter><![CDATA[
-          if (this._popup == val) {
-            return val;
-          }
-
           let events = [
             "popupshowing",
             "popuphidden",
           ];
           if (this._popup) {
             for (let event of events) {
               this._popup.removeEventListener(event, this);
             }
@@ -1248,29 +1261,27 @@
           if (val && val.state != "closed") {
             this._rebuild();
           }
           return val;
         ]]></setter>
       </property>
 
       <field name="_textbox">null</field>
+      <field name="_textboxWidth">0</field>
 
       <!-- The textbox associated with the one-offs.  Set this to a textbox to
            automatically keep the related one-offs UI up to date.  Otherwise you
            can leave it null/undefined, and in that case you should update the
            query property manually. -->
       <property name="textbox">
         <getter><![CDATA[
           return this._textbox;
         ]]></getter>
         <setter><![CDATA[
-          if (this._textbox == val) {
-            return val;
-          }
           if (this._textbox) {
             this._textbox.removeEventListener("input", this);
           }
           if (val) {
             val.addEventListener("input", this);
           }
           return this._textbox = val;
         ]]></setter>
@@ -1302,17 +1313,37 @@
 
       <!-- The selected one-off, a xul:button, including the add-engine button
            and the search-settings button.  Null if no one-off is selected. -->
       <property name="selectedButton">
         <getter><![CDATA[
           return this._selectedButton;
         ]]></getter>
         <setter><![CDATA[
-          this._changeVisuallySelectedButton(val, true);
+          if (val && val.classList.contains("dummy")) {
+            // Never select dummy buttons.
+            val = null;
+          }
+          if (this._selectedButton) {
+            this._selectedButton.removeAttribute("selected");
+          }
+          if (val) {
+            val.setAttribute("selected", "true");
+          }
+          this._selectedButton = val;
+          this._updateStateForButton(null);
+          if (val && !val.engine) {
+            // If the button doesn't have an engine, then clear the popup's
+            // selection to indicate that pressing Return while the button is
+            // selected will do the button's command, not search.
+            this.popup.selectedIndex = -1;
+          }
+          let event = document.createEvent("Events");
+          event.initEvent("SelectedOneOffButtonChanged", true, false);
+          this.dispatchEvent(event);
           return val;
         ]]></setter>
       </property>
 
       <!-- The index of the selected one-off, including the add-engine button
            and the search-settings button.  -1 if no one-off is selected. -->
       <property name="selectedButtonIndex">
         <getter><![CDATA[
@@ -1326,42 +1357,37 @@
         ]]></getter>
         <setter><![CDATA[
           let buttons = this.getSelectableButtons(true);
           this.selectedButton = buttons[val];
           return val;
         ]]></setter>
       </property>
 
-      <!-- The visually selected one-off is the same as the selected one-off
-           unless a one-off is moused over.  In that case, the visually selected
-           one-off is the moused-over one-off, which may be different from the
-           selected one-off.  The visually selected one-off is always the one
-           that is visually highlighted.  Includes the add-engine button and the
-           search-settings button.  A xul:button. -->
-      <property name="visuallySelectedButton" readonly="true">
-        <getter><![CDATA[
-          return this.getSelectableButtons(true).find(button => {
-            return button.getAttribute("selected") == "true";
-          });
-        ]]></getter>
-      </property>
-
       <property name="compact" readonly="true">
         <getter><![CDATA[
           return this.getAttribute("compact") == "true";
         ]]></getter>
       </property>
 
-      <property name="settingsButton" readonly="true">
-        <getter><![CDATA[
-          let id = this.compact ? "search-settings-compact" : "search-settings";
-          return document.getAnonymousElementByAttribute(this, "anonid", id);
-        ]]></getter>
-      </property>
+      <field name="buttons" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs");
+      </field>
+      <field name="header" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header");
+      </field>
+      <field name="addEngines" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
+      </field>
+      <field name="settingsButton" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
+      </field>
+      <field name="settingsButtonCompact" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
+      </field>
 
       <field name="_bundle">null</field>
 
       <property name="bundle" readonly="true">
         <getter><![CDATA[
           if (!this._bundle) {
             const kBundleURI = "chrome://browser/locale/search.properties";
             this._bundle = Services.strings.createBundle(kBundleURI);
@@ -1384,16 +1410,20 @@
         menu.addEventListener("popupshown", aEvent => {
           this._ignoreMouseEvents = true;
           aEvent.stopPropagation();
         });
         menu.addEventListener("popuphidden", aEvent => {
           this._ignoreMouseEvents = false;
           aEvent.stopPropagation();
         });
+
+        // Add weak referenced observers to invalidate our cached list of engines.
+        Services.prefs.addObserver("browser.search.hiddenOneOffs", this, true);
+        Services.obs.addObserver(this, "browser-search-engine-modified", true);
       ]]></constructor>
 
       <!-- This handles events outside the one-off buttons, like on the popup
            and textbox. -->
       <method name="handleEvent">
         <parameter name="event"/>
         <body><![CDATA[
           switch (event.type) {
@@ -1403,142 +1433,168 @@
               // actually what the user typed (e.g., it's autofilled, or it's a
               // mozaction URI), the consumer has some way of providing it.
               this.query = event.target.oneOffSearchQuery || event.target.value;
               break;
             case "popupshowing":
               this._rebuild();
               break;
             case "popuphidden":
-              Services.tm.mainThread.dispatch(() => {
+              Services.tm.dispatchToMainThread(() => {
                 this.selectedButton = null;
                 this._contextEngine = null;
-              }, Ci.nsIThread.DISPATCH_NORMAL);
+              });
               break;
           }
         ]]></body>
       </method>
 
+      <method name="observe">
+        <parameter name="aEngine"/>
+        <parameter name="aTopic"/>
+        <parameter name="aData"/>
+        <body><![CDATA[
+          // Make sure the engine list is refetched next time it's needed.
+          this._engines = null;
+        ]]></body>
+      </method>
+
       <method name="showSettings">
         <body><![CDATA[
           BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin);
-          openPreferences("paneGeneral");
+          openPreferences("general-search", {origin: "contentSearch"});
           // If the preference tab was already selected, the panel doesn't
           // close itself automatically.
           this.popup.hidePopup();
         ]]></body>
       </method>
 
       <!-- Updates the parts of the UI that show the query string. -->
       <method name="_updateAfterQueryChanged">
         <body><![CDATA[
           let headerSearchText =
             document.getAnonymousElementByAttribute(this, "anonid",
                                                     "searchbar-oneoffheader-searchtext");
-          let headerPanel =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "search-panel-one-offs-header");
-          let list = document.getAnonymousElementByAttribute(this, "anonid",
-                                                             "search-panel-one-offs");
           headerSearchText.setAttribute("value", this.query);
           let groupText;
           let isOneOffSelected =
             this.selectedButton &&
             this.selectedButton.classList.contains("searchbar-engine-one-off-item");
           // Typing de-selects the settings or opensearch buttons at the bottom
           // of the search panel, as typing shows the user intends to search.
           if (this.selectedButton && !isOneOffSelected)
             this.selectedButton = null;
           if (this.query) {
             groupText = headerSearchText.previousSibling.value +
                         '"' + headerSearchText.value + '"' +
                         headerSearchText.nextSibling.value;
             if (!isOneOffSelected)
-              headerPanel.selectedIndex = 1;
+              this.header.selectedIndex = 1;
           } else {
             let noSearchHeader =
               document.getAnonymousElementByAttribute(this, "anonid",
                                                       "searchbar-oneoffheader-search");
             groupText = noSearchHeader.value;
             if (!isOneOffSelected)
-              headerPanel.selectedIndex = 0;
+              this.header.selectedIndex = 0;
           }
-          list.setAttribute("aria-label", groupText);
+          this.buttons.setAttribute("aria-label", groupText);
         ]]></body>
       </method>
 
+      <field name="_engines">null</field>
+      <property name="engines" readonly="true">
+        <getter><![CDATA[
+          if (this._engines)
+            return this._engines;
+          let currentEngineNameToIgnore;
+          if (!this.getAttribute("includecurrentengine"))
+            currentEngineNameToIgnore = Services.search.currentEngine.name;
+
+          let pref = Services.prefs.getStringPref("browser.search.hiddenOneOffs");
+          let hiddenList = pref ? pref.split(",") : [];
+
+          this._engines = Services.search.getVisibleEngines().filter(e => {
+            let name = e.name;
+            return (!currentEngineNameToIgnore ||
+                    name != currentEngineNameToIgnore) &&
+                   !hiddenList.includes(name);
+          });
+
+          return this._engines;
+        ]]></getter>
+      </property>
+
       <!-- Builds all the UI. -->
       <method name="_rebuild">
         <body><![CDATA[
           // Update the 'Search for <keywords> with:" header.
           this._updateAfterQueryChanged();
 
-          let list = document.getAnonymousElementByAttribute(this, "anonid",
-                                                             "search-panel-one-offs");
-
           // Handle opensearch items. This needs to be done before building the
           // list of one off providers, as that code will return early if all the
           // alternative engines are hidden.
-          this._rebuildAddEngineList();
+          // Skip this in compact mode, ie. for the urlbar.
+          if (!this.compact)
+            this._rebuildAddEngineList();
 
-          let settingsButton =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "search-settings-compact");
+          // Check if the one-off buttons really need to be rebuilt.
+          if (this._textbox) {
+            // We can't get a reliable value for the popup width without flushing,
+            // but the popup width won't change if the textbox width doesn't.
+            let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                 .getInterface(Ci.nsIDOMWindowUtils);
+            let textboxWidth =
+              DOMUtils.getBoundsWithoutFlushing(this._textbox).width;
+            // We can return early if neither the list of engines nor the panel
+            // width has changed.
+            if (this._engines && this._textboxWidth == textboxWidth) {
+              return;
+            }
+            this._textboxWidth = textboxWidth;
+          }
+
           // Finally, build the list of one-off buttons.
-          while (list.firstChild != settingsButton)
-            list.firstChild.remove();
+          while (this.buttons.firstChild != this.settingsButtonCompact)
+            this.buttons.firstChild.remove();
           // Remove the trailing empty text node introduced by the binding's
           // content markup above.
-          if (settingsButton.nextSibling)
-            settingsButton.nextSibling.remove();
-
-          let Preferences =
-            Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
-          let pref = Preferences.get("browser.search.hiddenOneOffs");
-          let hiddenList = pref ? pref.split(",") : [];
+          if (this.settingsButtonCompact.nextSibling)
+            this.settingsButtonCompact.nextSibling.remove();
 
-          let currentEngineName = Services.search.currentEngine.name;
-          let includeCurrentEngine = this.getAttribute("includecurrentengine");
-          let engines = Services.search.getVisibleEngines().filter(e => {
-            return (includeCurrentEngine || e.name != currentEngineName) &&
-                   !hiddenList.includes(e.name);
-          });
+          let engines = this.engines;
+          let oneOffCount = engines.length;
 
-          let header = document.getAnonymousElementByAttribute(this, "anonid",
-                                                               "search-panel-one-offs-header")
           // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
-          header.hidden = list.collapsed = !engines.length;
+          this.header.hidden = this.buttons.collapsed = !oneOffCount;
 
-          if (!engines.length)
+          if (!oneOffCount)
             return;
 
           let panelWidth = parseInt(this.popup.clientWidth);
           // The + 1 is because the last button doesn't have a right border.
           let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
           let buttonWidth = Math.floor(panelWidth / enginesPerRow);
           // There will be an emtpy area of:
           //   panelWidth - enginesPerRow * buttonWidth  px
           // at the end of each row.
 
           // If the <description> tag with the list of search engines doesn't have
           // a fixed height, the panel will be sized incorrectly, causing the bottom
           // of the suggestion <tree> to be hidden.
-          let oneOffCount = engines.length;
           if (this.compact)
             ++oneOffCount;
           let rowCount = Math.ceil(oneOffCount / enginesPerRow);
           let height = rowCount * 33; // 32px per row, 1px border.
-          list.setAttribute("height", height + "px");
+          this.buttons.setAttribute("height", height + "px");
 
           // Ensure we can refer to the settings buttons by ID:
-          let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
-          settingsEl.id = this.telemetryOrigin + "-anon-search-settings";
-          let compactSettingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
-          compactSettingsEl.id = this.telemetryOrigin +
-                                 "-anon-search-settings-compact";
+          let origin = this.telemetryOrigin;
+          this.settingsButton.id = origin + "-anon-search-settings";
+          this.settingsButtonCompact.id = origin + "-anon-search-settings-compact";
 
           const kXULNS =
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
           let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow);
           for (let i = 0; i < engines.length; ++i) {
             let engine = engines[i];
             let button = document.createElementNS(kXULNS, "button");
@@ -1557,33 +1613,33 @@
               button.classList.add("last-of-row");
 
             if (i + 1 == engines.length)
               button.classList.add("last-engine");
 
             if (i >= oneOffCount + dummyItems - enginesPerRow)
               button.classList.add("last-row");
 
-            list.insertBefore(button, settingsButton);
+            this.buttons.insertBefore(button, this.settingsButtonCompact);
           }
 
           let hasDummyItems = !!dummyItems;
           while (dummyItems) {
             let button = document.createElementNS(kXULNS, "button");
             button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
             button.setAttribute("width", buttonWidth);
 
             if (!--dummyItems)
               button.classList.add("last-of-row");
 
-            list.insertBefore(button, settingsButton);
+            this.buttons.insertBefore(button, this.settingsButtonCompact);
           }
 
           if (this.compact) {
-            this.settingsButton.setAttribute("width", buttonWidth);
+            this.settingsButtonCompact.setAttribute("width", buttonWidth);
             if (rowCount == 1 && hasDummyItems) {
               // When there's only one row, make the compact settings button
               // hug the right edge of the panel.  It may not due to the panel's
               // width not being an integral multiple of the button width.  (See
               // the "There will be an emtpy area" comment above.)  Increase the
               // width of the last dummy item by the remainder.
               //
               // There's one weird thing to guard against: when layout pixels
@@ -1595,51 +1651,42 @@
               let scale = window.QueryInterface(Ci.nsIInterfaceRequestor)
                                 .getInterface(Ci.nsIDOMWindowUtils)
                                 .screenPixelsPerCSSPixel;
               let remainder = panelWidth - (enginesPerRow * buttonWidth);
               if (Math.floor(scale) != scale) {
                 remainder--;
               }
               let width = remainder + buttonWidth;
-              let lastDummyItem = this.settingsButton.previousSibling;
+              let lastDummyItem = this.settingsButtonCompact.previousSibling;
               lastDummyItem.setAttribute("width", width);
             }
           }
         ]]></body>
       </method>
 
       <!-- If a page offers more than this number of engines, the add-engines
            menu button is shown, instead of showing the engines directly in the
            popup. -->
       <field name="_addEngineMenuThreshold">5</field>
 
       <method name="_rebuildAddEngineList">
         <body><![CDATA[
-        let list = document.getAnonymousElementByAttribute(this, "anonid",
-                                                           "add-engines");
+        let list = this.addEngines;
         while (list.firstChild) {
           list.firstChild.remove();
         }
 
         // Add a button for each engine that the page in the selected browser
-        // offers, but with the following exceptions:
-        //
-        // (1) Not when the one-offs are compact.  Compact one-offs are shown in
-        // the urlbar, and the add-engine buttons span the width of the popup,
-        // so if we added all the engines that a page offers, it could break the
-        // urlbar popup by offering a ton of engines.  We should probably make a
-        // smaller version of the buttons for compact one-offs.
-        //
-        // (2) Not when there are too many offered engines.  The popup isn't
-        // designed to handle too many (by scrolling for example), so a page
-        // could break the popup by offering too many.  Instead, add a single
-        // menu button with a submenu of all the engines.
+        // offers, except when there are too many offered engines.
+        // The popup isn't designed to handle too many (by scrolling for
+        // example), so a page could break the popup by offering too many.
+        // Instead, add a single menu button with a submenu of all the engines.
 
-        if (this.compact || !gBrowser.selectedBrowser.engines) {
+        if (!gBrowser.selectedBrowser.engines) {
           return;
         }
 
         const kXULNS =
           "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
         let engines = gBrowser.selectedBrowser.engines;
         let tooManyEngines = engines.length > this._addEngineMenuThreshold;
@@ -1695,18 +1742,18 @@
           let button = document.createElementNS(kXULNS, eltType);
           button.classList.add("addengine-item");
           button.id = this.telemetryOrigin + "-add-engine-" +
                       this._fixUpEngineNameForID(engine.title);
           let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
                                                        [engine.title], 1);
           button.setAttribute("label", label);
           button.setAttribute("crop", "end");
-          button.setAttribute("tooltiptext", engine.uri);
           button.setAttribute("tooltiptext", engine.title + "\n" + engine.uri);
+          button.setAttribute("uri", engine.uri);
           button.setAttribute("title", engine.title);
           if (engine.icon) {
             button.setAttribute("image", engine.icon);
           }
           if (tooManyEngines) {
             button.classList.add("menuitem-iconic");
           } else {
             button.setAttribute("pack", "start");
@@ -1733,94 +1780,85 @@
 
       <method name="_buttonForEngine">
         <parameter name="engine"/>
         <body><![CDATA[
           return document.getElementById(this._buttonIDForEngine(engine));
         ]]></body>
       </method>
 
-      <method name="_changeVisuallySelectedButton">
-        <parameter name="val"/>
-        <parameter name="aUpdateLogicallySelectedButton"/>
+      <!--
+        Updates the popup and textbox for the currently selected or moused-over
+        button.
+
+        @param mousedOverButton
+               The currently moused-over button, or null if there isn't one.
+      -->
+      <method name="_updateStateForButton">
+        <parameter name="mousedOverButton"/>
         <body><![CDATA[
-          let visuallySelectedButton = this.visuallySelectedButton;
-          if (visuallySelectedButton)
-            visuallySelectedButton.removeAttribute("selected");
+          let button = mousedOverButton;
 
-          let header =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "search-panel-one-offs-header");
-          // Avoid selecting dummy buttons.
-          if (val && !val.classList.contains("dummy")) {
-            val.setAttribute("selected", "true");
-            if (val.classList.contains("searchbar-engine-one-off-item") &&
-                val.engine) {
-              let headerEngineText =
-                document.getAnonymousElementByAttribute(this, "anonid",
-                                                        "searchbar-oneoffheader-engine");
-              header.selectedIndex = 2;
-              headerEngineText.value = val.engine.name;
-            } else {
-              header.selectedIndex = this.query ? 1 : 0;
-            }
-            if (this.textbox) {
-              this.textbox.setAttribute("aria-activedescendant", val.id);
-            }
-          } else {
-            val = null;
-            header.selectedIndex = this.query ? 1 : 0;
+          // Ignore dummy buttons.
+          if (button && button.classList.contains("dummy")) {
+            button = null;
+          }
+
+          // If there's no moused-over button, then the one-offs should reflect
+          // the selected button, if any.
+          button = button || this.selectedButton;
+
+          if (!button) {
+            this.header.selectedIndex = this.query ? 1 : 0;
             if (this.textbox) {
               this.textbox.removeAttribute("aria-activedescendant");
             }
+            return;
           }
 
-          if (aUpdateLogicallySelectedButton) {
-            this._selectedButton = val;
-            if (val && !val.engine) {
-              // If the button doesn't have an engine, then clear the popup's
-              // selection to indicate that pressing Return while the button is
-              // selected will do the button's command, not search.
-              this.popup.selectedIndex = -1;
-            }
-            let event = document.createEvent("Events");
-            event.initEvent("SelectedOneOffButtonChanged", true, false);
-            this.dispatchEvent(event);
+          if (button.classList.contains("searchbar-engine-one-off-item") &&
+              button.engine) {
+            let headerEngineText =
+              document.getAnonymousElementByAttribute(this, "anonid",
+                                                      "searchbar-oneoffheader-engine");
+            this.header.selectedIndex = 2;
+            headerEngineText.value = button.engine.name;
+          } else {
+            this.header.selectedIndex = this.query ? 1 : 0;
+          }
+          if (this.textbox) {
+            this.textbox.setAttribute("aria-activedescendant", button.id);
           }
         ]]></body>
       </method>
 
       <method name="getSelectableButtons">
         <parameter name="aIncludeNonEngineButtons"/>
         <body><![CDATA[
           let buttons = [];
-          let oneOff = document.getAnonymousElementByAttribute(this, "anonid",
-                                                               "search-panel-one-offs");
-          for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+          for (let oneOff = this.buttons.firstChild; oneOff; oneOff = oneOff.nextSibling) {
             // oneOff may be a text node since the list xul:description contains
             // whitespace and the compact settings button.  See the markup
             // above.  _rebuild removes text nodes, but it may not have been
             // called yet (because e.g. the popup hasn't been opened yet).
             if (oneOff.nodeType == Node.ELEMENT_NODE) {
               if (oneOff.classList.contains("dummy") ||
                   oneOff.classList.contains("search-setting-button-compact"))
                 break;
               buttons.push(oneOff);
             }
           }
 
-          if (!aIncludeNonEngineButtons)
-            return buttons;
+          if (aIncludeNonEngineButtons) {
+            for (let addEngine = this.addEngines.firstChild; addEngine; addEngine = addEngine.nextSibling) {
+              buttons.push(addEngine);
+            }
+            buttons.push(this.compact ? this.settingsButtonCompact : this.settingsButton);
+          }
 
-          let addEngine =
-            document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
-          for (addEngine = addEngine.firstChild; addEngine; addEngine = addEngine.nextSibling)
-            buttons.push(addEngine);
-
-          buttons.push(this.settingsButton);
           return buttons;
         ]]></body>
       </method>
 
       <method name="handleSearchCommand">
         <parameter name="aEvent"/>
         <parameter name="aEngine"/>
         <parameter name="aForceNewTab"/>
@@ -1854,86 +1892,66 @@
       </method>
 
       <!--
         Increments or decrements the index of the currently selected one-off.
 
         @param aForward
                If true, the index is incremented, and if false, the index is
                decremented.
+        @param aIncludeNonEngineButtons
+               If true, non-dummy buttons that do not have engines are included.
+               These buttons include the OpenSearch and settings buttons.  For
+               example, if the currently selected button is an engine button,
+               the next button is the settings button, and you pass true for
+               aForward, then passing true for this value would cause the
+               settings to be selected.  Passing false for this value would
+               cause the selection to clear or wrap around, depending on what
+               value you passed for the aWrapAround parameter.
         @param aWrapAround
-               This has a couple of effects, depending on whether there is
-               currently a selection.
-               (1) If true and the last one-off is currently selected,
-               incrementing the index will cause the selection to be cleared and
-               this method to return true.  Calling advanceSelection again after
-               that (again with aForward=true) will select the first one-off.
-               Likewise if decrementing the index when the first one-off is
-               selected, except in the opposite direction of course.
-               (2) If true and there currently is no selection, decrementing the
-               index will cause the last one-off to become selected and this
-               method to return true.  Only the aForward=false case is affected
-               because it is always the case that if aForward=true and there
-               currently is no selection, the first one-off becomes selected and
-               this method returns true.
-        @param aCycleEngines
-               If true, only engine buttons are included.
+               If true, the selection wraps around between the first and last
+               buttons.
         @return True if the selection can continue to advance after this method
                 returns and false if not.
       -->
       <method name="advanceSelection">
         <parameter name="aForward"/>
+        <parameter name="aIncludeNonEngineButtons"/>
         <parameter name="aWrapAround"/>
-        <parameter name="aCycleEngines"/>
         <body><![CDATA[
-          let selectedButton = this.selectedButton;
-          let buttons = this.getSelectableButtons(aCycleEngines);
-
-          if (selectedButton) {
-            // cycle through one-off buttons.
-            let index = buttons.indexOf(selectedButton);
-            if (aForward)
-              ++index;
-            else
-              --index;
-
-            if (index >= 0 && index < buttons.length)
-              this.selectedButton = buttons[index];
-            else
-              this.selectedButton = null;
-
-            if (this.selectedButton || aWrapAround)
-              return true;
-
-            return false;
+          let buttons = this.getSelectableButtons(aIncludeNonEngineButtons);
+          let index;
+          if (this.selectedButton) {
+            let inc = aForward ? 1 : -1;
+            let oldIndex = buttons.indexOf(this.selectedButton);
+            index = ((oldIndex + inc) + buttons.length) % buttons.length;
+            if (!aWrapAround &&
+                ((aForward && index <= oldIndex) ||
+                 (!aForward && oldIndex <= index))) {
+              // The index has wrapped around, but wrapping around isn't
+              // allowed.
+              index = -1;
+            }
+          } else {
+            index = aForward ? 0 : buttons.length - 1;
           }
-
-          // If no selection, select the first button or ...
-          if (aForward) {
-            this.selectedButton = buttons[0];
-            return true;
-          }
-
-          if (!aForward && aWrapAround) {
-            // the last button.
-            this.selectedButton = buttons[buttons.length - 1];
-            return true;
-          }
-
-          return false;
+          this.selectedButton = index < 0 ? null : buttons[index];
         ]]></body>
       </method>
 
       <!--
         This handles key presses specific to the one-off buttons like Tab and
-        Alt-Up/Down, and Up/Down keys within the buttons.  Since one-off buttons
+        Alt+Up/Down, and Up/Down keys within the buttons.  Since one-off buttons
         are always used in conjunction with a list of some sort (in this.popup),
         it also handles Up/Down keys that cross the boundaries between list
         items and the one-off buttons.
 
+        If this method handles the key press, then event.defaultPrevented will
+        be true when it returns.
+
         @param event
                The key event.
         @param numListItems
                The number of items in the list.  The reason that this is a
                parameter at all is that the list may contain items at the end
                that should be ignored, depending on the consumer.  That's true
                for the urlbar for example.
         @param allowEmptySelection
@@ -1941,134 +1959,206 @@
                buttons contains a selection.  Pass false if either the list or
                the one-off buttons (or both) should always contain a selection.
         @param textboxUserValue
                When the last list item is selected and the user presses Down,
                the first one-off becomes selected and the textbox value is
                restored to the value that the user typed.  Pass that value here.
                However, if you pass true for allowEmptySelection, you don't need
                to pass anything for this parameter.  (Pass undefined or null.)
-        @return True if this method handled the keypress and false if not.  If
-                false, then you should let the autocomplete controller handle
-                the keypress.  The value of event.defaultPrevented will be the
-                same as this return value.
       -->
       <method name="handleKeyPress">
         <parameter name="event"/>
         <parameter name="numListItems"/>
         <parameter name="allowEmptySelection"/>
         <parameter name="textboxUserValue"/>
         <body><![CDATA[
           if (!this.popup) {
-            return false;
+            return;
           }
-
-          let stopEvent = false;
+          let handled = this._handleKeyPress(event, numListItems,
+                                             allowEmptySelection,
+                                             textboxUserValue);
+          if (handled) {
+            event.preventDefault();
+            event.stopPropagation();
+          }
+        ]]></body>
+      </method>
 
-          // Tab cycles through the one-offs and moves the focus out at the end.
-          // But only if non-Shift modifiers aren't also pressed, to avoid
-          // clobbering other shortcuts.
-          if (event.keyCode == KeyEvent.DOM_VK_TAB &&
-              !event.altKey &&
-              !event.ctrlKey &&
-              !event.metaKey &&
-              this.getAttribute("disabletab") != "true") {
-            stopEvent = this.advanceSelection(!event.shiftKey, false, true);
-          } else if (event.altKey &&
-                     (event.keyCode == KeyEvent.DOM_VK_DOWN ||
-                      event.keyCode == KeyEvent.DOM_VK_UP)) {
-            // Alt + up/down is very similar to (shift +) tab but differs in that
-            // it loops through the list, whereas tab will move the focus out.
-            stopEvent =
-              this.advanceSelection(event.keyCode == KeyEvent.DOM_VK_DOWN,
-                                    true, false);
-          } else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP) {
-            if (numListItems > 0) {
-              if (this.popup.selectedIndex > 0) {
-                // The autocomplete controller should handle this case.
-              } else if (this.popup.selectedIndex == 0) {
-                if (!allowEmptySelection) {
-                  // Wrap around the selection to the last one-off.
-                  this.selectedButton = null;
-                  this.popup.selectedIndex = -1;
-                  // Call advanceSelection after setting selectedIndex so that
-                  // screen readers see the newly selected one-off. Both trigger
-                  // accessibility events.
-                  this.advanceSelection(false, true, true);
-                  stopEvent = true;
-                }
-              } else {
-                let firstButtonSelected =
-                  this.selectedButton &&
-                  this.selectedButton == this.getSelectableButtons(true)[0];
-                if (firstButtonSelected) {
-                  this.selectedButton = null;
-                } else {
-                  stopEvent = this.advanceSelection(false, true, true);
-                }
-              }
-            } else {
-              stopEvent = this.advanceSelection(false, true, true);
-            }
-          } else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
-            if (numListItems > 0) {
-              if (this.popup.selectedIndex >= 0 &&
-                  this.popup.selectedIndex < numListItems - 1) {
-                // The autocomplete controller should handle this case.
-              } else if (this.popup.selectedIndex == numListItems - 1) {
-                this.selectedButton = null;
-                if (!allowEmptySelection) {
-                  this.popup.selectedIndex = -1;
-                  stopEvent = true;
-                }
-                if (this.textbox && typeof(textboxUserValue) == "string") {
-                  this.textbox.value = textboxUserValue;
-                }
-                // Call advanceSelection after setting selectedIndex so that
-                // screen readers see the newly selected one-off. Both trigger
-                // accessibility events.
-                this.advanceSelection(true, true, true);
-              } else {
-                let buttons = this.getSelectableButtons(true);
-                let lastButtonSelected =
-                  this.selectedButton &&
-                  this.selectedButton == buttons[buttons.length - 1];
-                if (lastButtonSelected) {
-                  this.selectedButton = null;
-                  stopEvent = allowEmptySelection;
-                } else if (this.selectedButton) {
-                  stopEvent = this.advanceSelection(true, true, true);
-                } else {
-                  // The autocomplete controller should handle this case.
-                }
-              }
-            } else {
-              stopEvent = this.advanceSelection(true, true, true);
-            }
-          } else if (this.selectedButton &&
-                     this.selectedButton.getAttribute("anonid") ==
-                       "addengine-menu-button" &&
-                     event.keyCode == KeyEvent.DOM_VK_RIGHT) {
+      <method name="_handleKeyPress">
+        <parameter name="event"/>
+        <parameter name="numListItems"/>
+        <parameter name="allowEmptySelection"/>
+        <parameter name="textboxUserValue"/>
+        <body><![CDATA[
+          if (event.keyCode == KeyEvent.DOM_VK_RIGHT &&
+              this.selectedButton &&
+              this.selectedButton.getAttribute("anonid") ==
+                "addengine-menu-button") {
             // If the add-engine overflow menu item is selected and the user
             // presses the right arrow key, open the submenu.  Unfortunately
             // handling the left arrow key -- to close the popup -- isn't
             // straightforward.  Once the popup is open, it consumes all key
             // events.  Setting ignorekeys=handled on it doesn't help, since the
             // popup handles all arrow keys.  Setting ignorekeys=true on it does
-            // mean that the popup no longer consumes the left arrow key, but then
-            // it no longer handles up/down keys to select items in the popup.
+            // mean that the popup no longer consumes the left arrow key, but
+            // then it no longer handles up/down keys to select items in the
+            // popup.
             this.selectedButton.open = true;
-            stopEvent = true;
+            return true;
+          }
+
+          // Handle the Tab key, but only if non-Shift modifiers aren't also
+          // pressed to avoid clobbering other shortcuts (like the Alt+Tab
+          // browser tab switcher).  The reason this uses getModifierState() and
+          // checks for "AltGraph" is that when you press Shift-Alt-Tab,
+          // event.altKey is actually false for some reason, at least on macOS.
+          // getModifierState("Alt") is also false, but "AltGraph" is true.
+          if (event.keyCode == KeyEvent.DOM_VK_TAB &&
+              !event.getModifierState("Alt") &&
+              !event.getModifierState("AltGraph") &&
+              !event.getModifierState("Control") &&
+              !event.getModifierState("Meta")) {
+            if (this.getAttribute("disabletab") == "true" ||
+                (event.shiftKey &&
+                  this.selectedButtonIndex <= 0) ||
+                (!event.shiftKey &&
+                 this.selectedButtonIndex ==
+                   this.getSelectableButtons(true).length - 1)) {
+              this.selectedButton = null;
+              return false;
+            }
+            this.popup.selectedIndex = -1;
+            this.advanceSelection(!event.shiftKey, true, false);
+            return !!this.selectedButton;
           }
 
-          if (stopEvent) {
-            event.preventDefault();
-            event.stopPropagation();
+          if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP) {
+            if (event.altKey) {
+              // Keep the currently selected result in the list (if any) as a
+              // secondary "alt" selection and move the selection up within the
+              // buttons.
+              this.advanceSelection(false, false, false);
+              return true;
+            }
+            if (numListItems == 0) {
+              this.advanceSelection(false, true, false);
+              return true;
+            }
+            if (this.popup.selectedIndex > 0) {
+              // Moving up within the list.  The autocomplete controller should
+              // handle this case.  A button may be selected, so null it.
+              this.selectedButton = null;
+              return false;
+            }
+            if (this.popup.selectedIndex == 0) {
+              // Moving up from the top of the list.
+              if (allowEmptySelection) {
+                // Let the autocomplete controller remove selection in the list
+                // and revert the typed text in the textbox.
+                return false;
+              }
+              // Wrap selection around to the last button.
+              if (this.textbox && typeof(textboxUserValue) == "string") {
+                this.textbox.value = textboxUserValue;
+              }
+              this.advanceSelection(false, true, true);
+              return true;
+            }
+            if (!this.selectedButton) {
+              // Moving up from no selection in the list or the buttons, back
+              // down to the last button.
+              this.advanceSelection(false, true, true);
+              return true;
+            }
+            if (this.selectedButtonIndex == 0) {
+              // Moving up from the buttons to the bottom of the list.
+              this.selectedButton = null;
+              return false;
+            }
+            // Moving up/left within the buttons.
+            this.advanceSelection(false, true, false);
             return true;
           }
+
+          if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
+            if (event.altKey) {
+              // Keep the currently selected result in the list (if any) as a
+              // secondary "alt" selection and move the selection down within
+              // the buttons.
+              this.advanceSelection(true, false, false);
+              return true;
+            }
+            if (numListItems == 0) {
+              this.advanceSelection(true, true, false);
+              return true;
+            }
+            if (this.popup.selectedIndex >= 0 &&
+                this.popup.selectedIndex < numListItems - 1) {
+              // Moving down within the list.  The autocomplete controller
+              // should handle this case.  A button may be selected, so null it.
+              this.selectedButton = null;
+              return false;
+            }
+            if (this.popup.selectedIndex == numListItems - 1) {
+              // Moving down from the last item in the list to the buttons.
+              this.selectedButtonIndex = 0;
+              if (allowEmptySelection) {
+                // Let the autocomplete controller remove selection in the list
+                // and revert the typed text in the textbox.
+                return false;
+              }
+              if (this.textbox && typeof(textboxUserValue) == "string") {
+                this.textbox.value = textboxUserValue;
+              }
+              this.popup.selectedIndex = -1;
+              return true;
+            }
+            if (this.selectedButton) {
+              let buttons = this.getSelectableButtons(true);
+              if (this.selectedButtonIndex == buttons.length - 1) {
+                // Moving down from the buttons back up to the top of the list.
+                this.selectedButton = null;
+                if (allowEmptySelection) {
+                  // Prevent the selection from wrapping around to the top of
+                  // the list by returning true, since the list currently has no
+                  // selection.  Nothing should be selected after handling this
+                  // Down key.
+                  return true;
+                }
+                return false;
+              }
+              // Moving down/right within the buttons.
+              this.advanceSelection(true, true, false);
+              return true;
+            }
+            return false;
+          }
+
+          if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_LEFT) {
+            if (this.selectedButton &&
+                (this.compact || this.selectedButton.engine)) {
+              // Moving left within the buttons.
+              this.advanceSelection(false, this.compact, true);
+              return true;
+            }
+            return false;
+          }
+
+          if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
+            if (this.selectedButton &&
+                (this.compact || this.selectedButton.engine)) {
+              // Moving right within the buttons.
+              this.advanceSelection(true, this.compact, true);
+              return true;
+            }
+            return false;
+          }
+
           return false;
         ]]></body>
       </method>
 
       <!--
         If the given event is related to the one-offs, this method records
         one-off telemetry for it.  this.telemetryOrigin will be appended to the
         computed source, so make sure you set that first.
@@ -2169,24 +2259,20 @@
 
       <handler event="mousemove"><![CDATA[
         let target = event.originalTarget;
 
         // Handle mouseover on the add-engine menu button and its popup items.
         if (target.getAttribute("anonid") == "addengine-menu-button" ||
             (target.localName == "menuitem" &&
              target.classList.contains("addengine-item"))) {
-          // Make the menu button visually selected.  It's highlighted in the
-          // CSS when the popup is open, but the popup doesn't open until a
-          // short timeout has elapsed.  Making the button visually selected now
-          // provides better feedback to the user.
           let menuButton = document.getAnonymousElementByAttribute(
             this, "anonid", "addengine-menu-button"
           );
-          this._changeVisuallySelectedButton(menuButton);
+          this._updateStateForButton(menuButton);
           this._addEngineMenuShouldBeOpen = true;
           this._resetAddEngineMenuTimeout();
           return;
         }
 
         if (target.localName != "button")
           return;
 
@@ -2195,53 +2281,43 @@
            return;
 
         let isOneOff =
           target.classList.contains("searchbar-engine-one-off-item") &&
           !target.classList.contains("dummy");
         if (isOneOff ||
             target.classList.contains("addengine-item") ||
             target.classList.contains("search-setting-button")) {
-          this._changeVisuallySelectedButton(target);
+          this._updateStateForButton(target);
         }
       ]]></handler>
 
       <handler event="mouseout"><![CDATA[
 
         let target = event.originalTarget;
 
         // Handle mouseout on the add-engine menu button and its popup items.
         if (target.getAttribute("anonid") == "addengine-menu-button" ||
             (target.localName == "menuitem" &&
              target.classList.contains("addengine-item"))) {
-          // The menu button will appear selected since the mouse is either over
-          // it or over one of the menu items in the popup.  Make it unselected.
-          this._changeVisuallySelectedButton(null);
+          this._updateStateForButton(null);
           this._addEngineMenuShouldBeOpen = false;
           this._resetAddEngineMenuTimeout();
           return;
         }
 
         if (target.localName != "button") {
           return;
         }
 
-        // Don't deselect the current button if the context menu is open.
+        // Don't update the mouseover state if the context menu is open.
         if (this._ignoreMouseEvents)
           return;
 
-        // Unfortunately this will fire before mouseover hits another item.
-        // If this button is selected, we replace that selection only if
-        // we're not moving to a different one-off item:
-        if (target.getAttribute("selected") == "true" &&
-            (!event.relatedTarget ||
-             !event.relatedTarget.classList.contains("searchbar-engine-one-off-item") ||
-             event.relatedTarget.classList.contains("dummy"))) {
-          this._changeVisuallySelectedButton(this.selectedButton);
-        }
+        this._updateStateForButton(null);
       ]]></handler>
 
       <handler event="click"><![CDATA[
         if (event.button == 2)
           return; // ignore right clicks.
 
         let button = event.originalTarget;
         let engine = button.engine;
@@ -2328,8 +2404,9 @@
 
         this._contextEngine = target.engine;
       ]]></handler>
     </handlers>
 
   </binding>
 
 </bindings>
+
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -121,17 +121,17 @@
                            onpopupshowing="document.getBindingParent(this).optionsShowing();">
               <xul:menuitem anonid="neverForLanguage"
                             oncommand="document.getBindingParent(this).neverForLanguage();"/>
               <xul:menuitem anonid="neverForSite"
                             oncommand="document.getBindingParent(this).neverForSite();"
                             label="&translation.options.neverForSite.label;"
                             accesskey="&translation.options.neverForSite.accesskey;"/>
               <xul:menuseparator/>
-              <xul:menuitem oncommand="openPreferences('paneGeneral');"
+              <xul:menuitem oncommand="openPreferences('paneContent', {origin:'menubar'});"
                             label="&translation.options.preferences.label;"
                             accesskey="&translation.options.preferences.accesskey;"/>
               <xul:menuitem class="subviewbutton panel-subview-footer"
                             oncommand="document.getBindingParent(this).openProviderAttribution();">
                 <xul:deck anonid="translationEngine" selectedIndex="0">
                   <xul:hbox class="translation-attribution">
                     <xul:label>&translation.options.attribution.beforeLogo;</xul:label>
                     <xul:image src="chrome://browser/content/microsoft-translator-attribution.png"
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -533,17 +533,17 @@ this.UITour = {
       }
 
       case "openPreferences": {
         if (typeof data.pane != "string" && typeof data.pane != "undefined") {
           log.warn("openPreferences: Invalid pane specified");
           return false;
         }
 
-        window.openPreferences(data.pane);
+        window.openPreferences(data.pane, { origin: "UITour" });
         break;
       }
 
       case "showFirefoxAccounts": {
         // 'signup' is the only action that makes sense currently, so we don't
         // accept arbitrary actions just to be safe...
         let p = new URLSearchParams("action=signup&entrypoint=uitour");
         // Call our helper to validate extraURLCampaignParams and populate URLSearchParams
--- a/browser/modules/AboutHome.jsm
+++ b/browser/modules/AboutHome.jsm
@@ -136,21 +136,21 @@ var AboutHome = {
         window.PlacesCommandHook.showPlacesOrganizer("History");
         break;
 
       case "AboutHome:Addons":
         window.BrowserOpenAddonsMgr();
         break;
 
       case "AboutHome:Sync":
-        window.openPreferences("paneSync", { urlParams: { entrypoint: "abouthome" } });
+        window.openPreferences("paneSync", { urlParams: { entrypoint: "abouthome" }, origin: "aboutHome"  });
         break;
 
       case "AboutHome:Settings":
-        window.openPreferences();
+        window.openPreferences(undefined, {origin: "aboutHome"});
         break;
 
       case "AboutHome:RequestUpdate":
         this.sendAboutHomeData(aMessage.target);
         break;
 
       case "AboutHome:MaybeShowAutoMigrationUndoNotification":
         AutoMigrate.maybeShowUndoNotification(aMessage.target);
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -415,17 +415,17 @@ this.ContentSearch = {
   },
 
   _onMessageSetCurrentEngine(msg, data) {
     Services.search.currentEngine = Services.search.getEngineByName(data);
   },
 
   _onMessageManageEngines(msg, data) {
     let browserWin = msg.target.ownerGlobal;
-    browserWin.openPreferences("paneGeneral");
+    browserWin.openPreferences("paneSearch", {origin: "contentSearch"});
   },
 
   _onMessageGetSuggestions: Task.async(function* (msg, data) {
     this._ensureDataHasProperties(data, [
       "engineName",
       "searchString",
     ]);
     let {engineName, searchString} = data;
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5678,16 +5678,25 @@
     "bug_numbers": [1335907],
     "alert_emails": ["jaws@mozilla.com"],
     "expires_in_version": "59",
     "kind": "categorical",
     "labels": ["unknown", "searchresults", "general", "applications", "privacy", "sync", "advanced"],
     "releaseChannelCollection": "opt-out",
     "description": "Count how often each preference category is opened."
   },
+  "FX_PREFERENCES_OPENED_VIA": {
+    "bug_numbers": [1330315],
+    "alert_emails": ["jaws@mozilla.com"],
+    "expires_in_version": "59",
+    "kind": "categorical",
+    "labels": ["aboutHome", "aboutTelemetry", "browserMedia", "commandLine", "commandLineLegacy", "ContainersCommand", "contentSearch", "dataReporting", "experimentsOpenPre", "fxa", "fxaSignedin", "fxaError", "offlineApps", "preferencesButton", "paneSync", "storageManager", "UITour", "menubar", "nsIObserver", "other"],
+    "releaseChannelCollection": "opt-out",
+    "description":"Count how the Preferences are opened."
+  },
   "INPUT_EVENT_RESPONSE_MS": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "bug_numbers": [1235908],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Time (ms) from the Input event being created to the end of it being handled"
--- a/toolkit/content/aboutTelemetry.js
+++ b/toolkit/content/aboutTelemetry.js
@@ -240,19 +240,19 @@ var Settings = {
             resource: "preferences_privacy",
           });
         } else {
           // Show the data choices preferences on desktop.
           let mainWindow = getMainWindowWithPreferencesPane();
           // The advanced subpanes are only supported in the old organization, which will
           // be removed by bug 1349689.
           if (Preferences.get("browser.preferences.useOldOrganization", false)) {
-            mainWindow.openAdvancedPreferences("dataChoicesTab");
+            mainWindow.openAdvancedPreferences("dataChoicesTab", {origin: "aboutTelemetry"});
           } else {
-            mainWindow.openPreferences("paneAdvanced");
+            mainWindow.openPreferences("paneAdvanced", {origin: "aboutTelemetry"});
           }
         }
       });
     }
   },
 
   detachObservers() {
     for (let setting of this.SETTINGS) {
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1538,19 +1538,19 @@ var gViewController = {
       isEnabled() {
         return !!getMainWindowWithPreferencesPane();
       },
       doCommand() {
         let mainWindow = getMainWindowWithPreferencesPane();
         // The advanced subpanes are only supported in the old organization, which will
         // be removed by bug 1349689.
         if (Preferences.get("browser.preferences.useOldOrganization", false)) {
-          mainWindow.openAdvancedPreferences("dataChoicesTab");
+          mainWindow.openAdvancedPreferences("dataChoicesTab", {origin: "experimentsOpenPre"});
         } else {
-          mainWindow.openPreferences("paneAdvanced");
+          mainWindow.openPreferences("paneAdvanced", {origin: "experimentsOpenPre"});
         }
       },
     },
 
     cmd_showUnsignedExtensions: {
       isEnabled() {
         return true;
       },