Bug 669905 - Searching in Downloads folder should limit the search to downloads.
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 19 Aug 2011 15:01:21 +0200
changeset 75557 55377b8a385a322d6bba26543159b3ba80836251
parent 75556 87e7c8dbba636345ac31e46ce96a9736754cb45c
child 75558 d103df4833c4f032085c051355a0ee5d917cdcb6
push id21040
push userbmo@edmorley.co.uk
push dateSun, 21 Aug 2011 18:16:59 +0000
treeherdermozilla-central@482742e6fff7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs669905
milestone9.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 669905 - Searching in Downloads folder should limit the search to downloads. r=dietrich
browser/components/places/content/places.js
browser/components/places/content/places.xul
browser/components/places/tests/browser/browser_library_search.js
browser/locales/en-US/chrome/browser/places/places.properties
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -272,27 +272,40 @@ var PlacesOrganizer = {
   },
 
   /**
    * Sets the search scope based on aNode's properties.
    * @param   aNode
    *          the node to set up scope from
    */
   _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
-    var itemId = aNode.itemId;
+    let itemId = aNode.itemId;
+
+    // Set default buttons status.
+    let bookmarksButton = document.getElementById("scopeBarAll");
+    bookmarksButton.hidden = false;
+    let downloadsButton = document.getElementById("scopeBarDownloads");
+    downloadsButton.hidden = true;
+
     if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
         itemId == PlacesUIUtils.leftPaneQueries["History"]) {
       PlacesQueryBuilder.setScope("history");
     }
-    // Default to All Bookmarks for all other nodes, per bug 469437.
-    else
+    else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) {
+      downloadsButton.hidden = false;
+      bookmarksButton.hidden = true;
+      PlacesQueryBuilder.setScope("downloads");
+    }
+    else {
+      // Default to All Bookmarks for all other nodes, per bug 469437.
       PlacesQueryBuilder.setScope("bookmarks");
+    }
 
     // Enable or disable the folder scope button.
-    var folderButton = document.getElementById("scopeBarFolder");
+    let folderButton = document.getElementById("scopeBarFolder");
     folderButton.hidden = !PlacesUtils.nodeIsFolder(aNode) ||
                           itemId == PlacesUIUtils.allBookmarksFolderId;
   },
 
   /**
    * Handle clicks on the tree.
    * Single Left click, right click or modified click do not result in any
    * special action, since they're related to selection.
@@ -896,19 +909,31 @@ var PlacesSearchBox = {
         var query = PlacesUtils.history.getNewQuery();
         query.searchTerms = filterString;
         var options = currentOptions.clone();
         // Make sure we're getting uri results.
         options.resultType = currentOptions.RESULT_TYPE_URI;
         options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
         content.load([query], options);
       }
-      else
+      else {
         content.applyFilter(filterString);
+      }
       break;
+    case "downloads": {
+        let query = PlacesUtils.history.getNewQuery();
+        query.searchTerms = filterString;
+        query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1);
+        let options = currentOptions.clone();
+        // Make sure we're getting uri results.
+        options.resultType = currentOptions.RESULT_TYPE_URI;
+        options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+        content.load([query], options);
+      break;
+    }
     default:
       throw "Invalid filterCollection on search";
       break;
     }
 
     PlacesSearchBox.showSearchUI();
 
     // Update the details panel
@@ -928,27 +953,38 @@ var PlacesSearchBox = {
    */
   findCurrent: function PSB_findCurrent() {
     PlacesQueryBuilder.setScope("collection");
     this.focus();
   },
 
   /**
    * Updates the display with the title of the current collection.
-   * @param   title
+   * @param   aTitle
    *          The title of the current collection.
    */
-  updateCollectionTitle: function PSB_updateCollectionTitle(title) {
-    if (title)
-      this.searchFilter.placeholder =
-        PlacesUIUtils.getFormattedString("searchCurrentDefault", [title]);
-    else
-      this.searchFilter.placeholder = this.filterCollection == "history" ?
-                                      PlacesUIUtils.getString("searchHistory") :
-                                      PlacesUIUtils.getString("searchBookmarks");
+  updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) {
+    let title = "";
+    if (aTitle) {
+      title = PlacesUIUtils.getFormattedString("searchCurrentDefault",
+                                               [aTitle]);
+    }
+    else {
+      switch(this.filterCollection) {
+        case "history":
+          title = PlacesUIUtils.getString("searchHistory");
+          break;
+        case "downloads":
+          title = PlacesUIUtils.getString("searchDownloads");
+          break;
+        default:
+          title = PlacesUIUtils.getString("searchBookmarks");                                    
+      }
+    }
+    this.searchFilter.placeholder = title;
   },
 
   /**
    * Gets/sets the active collection from the dropdown menu.
    */
   get filterCollection() {
     return this.searchFilter.getAttribute("collection");
   },
@@ -1020,32 +1056,36 @@ var PlacesQueryBuilder = {
   onScopeSelected: function PQB_onScopeSelected(aButton) {
     switch (aButton.id) {
     case "scopeBarHistory":
       this.setScope("history");
       break;
     case "scopeBarFolder":
       this.setScope("collection");
       break;
+    case "scopeBarDownloads":
+      this.setScope("downloads");
+      break;
     case "scopeBarAll":
       this.setScope("bookmarks");
       break;
     default:
       throw "Invalid search scope button ID";
       break;
     }
   },
 
   /**
    * Sets the search scope.  This can be called when no search is active, and
    * in that case, when the user does begin a search aScope will be used (see
    * PSB_search()).  If there is an active search, it's performed again to
    * update the content tree.
    * @param   aScope
-   *          the search scope, "bookmarks", "collection", or "history"
+   *          The search scope: "bookmarks", "collection", "downloads" or
+   *          "history".
    */
   setScope: function PQB_setScope(aScope) {
     // Determine filterCollection, folders, and scopeButtonId based on aScope.
     var filterCollection;
     var folders = [];
     var scopeButtonId;
     switch (aScope) {
     case "history":
@@ -1067,16 +1107,20 @@ var PlacesQueryBuilder = {
       // selected node, choose bookmarks scope.
     case "bookmarks":
       filterCollection = "bookmarks";
       scopeButtonId = "scopeBarAll";
       folders.push(PlacesUtils.bookmarksMenuFolderId,
                    PlacesUtils.toolbarFolderId,
                    PlacesUtils.unfiledBookmarksFolderId);
       break;
+    case "downloads":
+      filterCollection = "downloads";
+      scopeButtonId = "scopeBarDownloads";
+      break;
     default:
       throw "Invalid search scope";
       break;
     }
 
     // Check the appropriate scope button in the scope bar.
     document.getElementById(scopeButtonId).checked = true;
 
--- a/browser/components/places/content/places.xul
+++ b/browser/components/places/content/places.xul
@@ -407,28 +407,26 @@
       <toolbox id="searchModifiers" hidden="true">
         <toolbar id="organizerScopeBar" class="chromeclass-toolbar" align="center">
           <label id="scopeBarTitle" value="&search.in.label;"/>
           <toolbarbutton id="scopeBarAll" class="small-margin"
                          type="radio" group="scopeBar"
                          oncommand="PlacesQueryBuilder.onScopeSelected(this);"
                          label="&search.scopeBookmarks.label;"
                          accesskey="&search.scopeBookmarks.accesskey;"/>
-          <!--
+          <toolbarbutton id="scopeBarHistory" class="small-margin"
+                         type="radio" group="scopeBar"
+                         oncommand="PlacesQueryBuilder.onScopeSelected(this);"
+                         label="&search.scopeHistory.label;"
+                         accesskey="&search.scopeHistory.accesskey;"/>
           <toolbarbutton id="scopeBarDownloads" class="small-margin"
                          type="radio" group="scopeBar"
                          oncommand="PlacesQueryBuilder.onScopeSelected(this);"
                          label="&search.scopeDownloads.label;"
                          accesskey="&search.scopeDownloads.accesskey;"/>
-          -->
-          <toolbarbutton id="scopeBarHistory" class="small-margin"
-                         type="radio" group="scopeBar"
-                         oncommand="PlacesQueryBuilder.onScopeSelected(this);"
-                         label="&search.scopeHistory.label;"
-                         accesskey="&search.scopeHistory.accesskey;"/>
           <toolbarbutton id="scopeBarFolder" class="small-margin"
                          type="radio" group="scopeBar"
                          oncommand="PlacesQueryBuilder.onScopeSelected(this);"
                          accesskey="&search.scopeFolder.accesskey;"
                          emptytitle="&search.scopeFolder.label;" flex="1"/>
           <!-- The folder scope button should flex but not take up more room
                 than its label needs.  The only simple way to do that is to
                 set a really big flex on the spacer, e.g., 2^31 - 1. -->
--- a/browser/components/places/tests/browser/browser_library_search.js
+++ b/browser/components/places/tests/browser/browser_library_search.js
@@ -56,115 +56,127 @@
  *   4. if the folder scope button is enabled clicks it,
  *   5. resets the search and ensures that the content tree is correct and that
  *      the search UI is hidden, and
  *   6. if folder scope was clicked, searches again and ensures folder scope
  *      remains selected.
  */
 
 const TEST_URL = "http://dummy.mozilla.org/";
+const TEST_DOWNLOAD_URL = "http://dummy.mozilla.org/dummy.pdf";
 
-// Add your tests here.  Each is a function that's called by testHelper().
-var testCases = [
+let gLibrary;
+
+let testCases = [
+  function allBookmarksScope() {
+    let defScope = getDefaultScope(PlacesUIUtils.allBookmarksFolderId);
+    search(PlacesUIUtils.allBookmarksFolderId, "dummy", defScope);
+    ok(!selectScope("scopeBarFolder"),
+       "Folder scope should be disabled for All Bookmarks");
+    ok(selectScope("scopeBarAll"),
+       "Bookmarks scope should be enabled for All Bookmarks");
+    resetSearch("scopeBarAll");
+  },
 
-  // All Bookmarks
-  function () {
-    var defScope = getDefaultScope(PlacesUIUtils.allBookmarksFolderId);
-    search(PlacesUIUtils.allBookmarksFolderId, "dummy", defScope);
-    is(selectScope("scopeBarFolder"), false,
-       "Folder scope should be disabled for All Bookmarks");
+  function historyScope() {
+    let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["History"]);
+    search(PlacesUIUtils.leftPaneQueries["History"], "dummy", defScope);
+    ok(!selectScope("scopeBarFolder"),
+       "Folder scope should be disabled for History");
+    ok(selectScope("scopeBarAll"),
+       "Bookmarks scope should be enabled for History");
+    resetSearch("scopeBarAll");
+  },
+
+  function downloadsScope() {
+    let defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["Downloads"]);
+    search(PlacesUIUtils.leftPaneQueries["Downloads"], "dummy", defScope);
+    ok(!selectScope("scopeBarFolder"),
+       "Folder scope should be disabled for Downloads");
+    ok(!selectScope("scopeBarAll"),
+       "Bookmarks scope should be disabled for Downloads");
     resetSearch(defScope);
   },
 
-  // History
-  function () {
-    var defScope = getDefaultScope(PlacesUIUtils.leftPaneQueries["History"]);
-    search(PlacesUIUtils.leftPaneQueries["History"], "dummy", defScope);
-    is(selectScope("scopeBarFolder"), false,
-       "Folder scope should be disabled for History");
-    resetSearch(defScope);
-  },
-
-  // Toolbar folder
-  function () {
-    var defScope = getDefaultScope(bmsvc.toolbarFolder);
-    search(bmsvc.toolbarFolder, "dummy", defScope);
-    is(selectScope("scopeBarFolder"), true,
+  function toolbarFolderScope() {
+    let defScope = getDefaultScope(PlacesUtils.toolbarFolderId);
+    search(PlacesUtils.toolbarFolderId, "dummy", defScope);
+    ok(selectScope("scopeBarAll"),
+       "Bookmarks scope should be enabled for toolbar folder");
+    ok(selectScope("scopeBarFolder"),
        "Folder scope should be enabled for toolbar folder");
     // Ensure that folder scope is still selected after resetting and searching
     // again.
     resetSearch("scopeBarFolder");
-    search(bmsvc.toolbarFolder, "dummy", "scopeBarFolder");
+    search(PlacesUtils.toolbarFolderId, "dummy", "scopeBarFolder");
   },
 
-  // A regular non-root subfolder
-  function () {
-    var folderId = bmsvc.createFolder(bmsvc.toolbarFolder,
-                                      "dummy folder",
-                                      bmsvc.DEFAULT_INDEX);
-    var defScope = getDefaultScope(folderId);
+  function subFolderScope() {
+    let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
+                                                      "dummy folder",
+                                                      PlacesUtils.bookmarks.DEFAULT_INDEX);
+    let defScope = getDefaultScope(folderId);
     search(folderId, "dummy", defScope);
-    is(selectScope("scopeBarFolder"), true,
+    ok(selectScope("scopeBarAll"),
+       "Bookmarks scope should be enabled for regularfolder");
+    ok(selectScope("scopeBarFolder"),
        "Folder scope should be enabled for regular subfolder");
     // Ensure that folder scope is still selected after resetting and searching
     // again.
     resetSearch("scopeBarFolder");
     search(folderId, "dummy", "scopeBarFolder");
-    bmsvc.removeItem(folderId);
+    PlacesUtils.bookmarks.removeItem(folderId);
   },
 ];
 
 ///////////////////////////////////////////////////////////////////////////////
 
-var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
-              getService(Ci.nsINavBookmarksService);
-var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
-                getService(Ci.nsINavHistoryService);
-var libraryWin;
-
-///////////////////////////////////////////////////////////////////////////////
-
 /**
  * Returns the default search scope for a given folder.
  *
  * @param  aFolderId
  *         the item ID of a node in the left pane's tree
  * @return the default scope when the folder is newly selected
  */
 function getDefaultScope(aFolderId) {
-  return aFolderId === PlacesUIUtils.leftPaneQueries["History"] ?
-         "scopeBarHistory" :
-         "scopeBarAll";
+  switch (aFolderId) {
+    case PlacesUIUtils.leftPaneQueries["History"]:
+      return "scopeBarHistory"
+    case PlacesUIUtils.leftPaneQueries["Downloads"]:
+      return "scopeBarDownloads";
+    default:
+      return "scopeBarAll";
+  }
 }
 
 /**
  * Returns the ID of the search scope button that is currently checked.
  *
  * @return the ID of the selected scope folder button
  */
 function getSelectedScopeButtonId() {
-  var doc = libraryWin.document;
-  var scopeButtons = doc.getElementById("organizerScopeBar").childNodes;
+  let doc = gLibrary.document;
+  let scopeButtons = doc.getElementById("organizerScopeBar").childNodes;
   for (let i = 0; i < scopeButtons.length; i++) {
     if (scopeButtons[i].checked)
       return scopeButtons[i].id;
   }
   return null;
 }
 
 /**
  * Returns the single nsINavHistoryQuery represented by a given place URI.
  *
  * @param  aPlaceURI
  *         a URI that represents a single query
  * @return an nsINavHistoryQuery object
  */
 function queryStringToQuery(aPlaceURI) {
-  var queries = {};
-  histsvc.queryStringToQueries(aPlaceURI, queries, {}, {});
+  let queries = {};
+  PlacesUtils.history.queryStringToQueries(aPlaceURI, queries, {}, {});
   return queries.value[0];
 }
 
 /**
  * Resets the search by clearing the search box's text and ensures that the
  * search scope remains as expected.
  *
  * @param  aExpectedScopeButtonId
@@ -183,61 +195,76 @@ function resetSearch(aExpectedScopeButto
  * @param  aFolderId
  *         the item ID of a node in the left pane's tree
  * @param  aSearchStr
  *         the search text; may be empty to reset the search
  * @param  aExpectedScopeButtonId
  *         after searching the selected scope button should be this
  */
 function search(aFolderId, aSearchStr, aExpectedScopeButtonId) {
-  var doc = libraryWin.document;
-  var folderTree = doc.getElementById("placesList");
-  var contentTree = doc.getElementById("placeContent");
+  let doc = gLibrary.document;
+  let folderTree = doc.getElementById("placesList");
+  let contentTree = doc.getElementById("placeContent");
 
   // First, ensure that selecting the folder in the left pane updates the
   // content tree properly.
   if (aFolderId) {
     folderTree.selectItems([aFolderId]);
     isnot(folderTree.selectedNode, null,
        "Sanity check: left pane tree should have selection after selecting!");
 
     // getFolders() on a History query returns an empty array, so no use
     // comparing against aFolderId in that case.
-    if (aFolderId !== PlacesUIUtils.leftPaneQueries["History"]) {
+    if (aFolderId !== PlacesUIUtils.leftPaneQueries["History"] &&
+        aFolderId !== PlacesUIUtils.leftPaneQueries["Downloads"]) {
       // contentTree.place should be equal to contentTree.result.root.uri,
       // but it's not until bug 476952 is fixed.
-      var query = queryStringToQuery(contentTree.result.root.uri);
+      let query = queryStringToQuery(contentTree.result.root.uri);
       is(query.getFolders()[0], aFolderId,
          "Content tree's folder should be what was selected in the left pane");
     }
   }
 
   // Second, ensure that searching updates the content tree and search UI
   // properly.
-  var searchBox = doc.getElementById("searchFilter");
+  let searchBox = doc.getElementById("searchFilter");
   searchBox.value = aSearchStr;
-  libraryWin.PlacesSearchBox.search(searchBox.value);
-  query = queryStringToQuery(contentTree.result.root.uri);
+  gLibrary.PlacesSearchBox.search(searchBox.value);
+  let query = queryStringToQuery(contentTree.result.root.uri);
   if (aSearchStr) {
     is(query.searchTerms, aSearchStr,
        "Content tree's searchTerms should be text in search box");
     is(doc.getElementById("searchModifiers").hidden, false,
        "Scope bar should not be hidden after searching");
-    if (getSelectedScopeButtonId() == "scopeBarHistory" ||
-        getSelectedScopeButtonId() == "scopeBarAll" ||
-        aFolderId == PlacesUtils.bookmarks.unfiledBookmarksFolder) {
+
+    let scopeButtonId = getSelectedScopeButtonId();
+    if (scopeButtonId == "scopeBarDownloads" ||
+        scopeButtonId == "scopeBarHistory" ||
+        scopeButtonId == "scopeBarAll" ||
+        aFolderId == PlacesUtils.unfiledBookmarksFolderId) {
       // Check that the target node exists in the tree's search results.
-      var node = null;
-      for (var i = 0; i < contentTree.view.rowCount; i++) {
+      let url, count;
+      if (scopeButtonId == "scopeBarDownloads") {
+        url = TEST_DOWNLOAD_URL;
+        count = 1;
+      }
+      else {
+        url = TEST_URL;
+        count = scopeButtonId == "scopeBarHistory" ? 2 : 1;
+      }
+      is(contentTree.view.rowCount, count, "Found correct number of results");
+
+      let node = null;
+      for (let i = 0; i < contentTree.view.rowCount; i++) {
         node = contentTree.view.nodeForTreeIndex(i);
-        if (node.uri === TEST_URL)
+        if (node.uri === url)
           break;
       }
       isnot(node, null, "At least the target node should be in the tree");
-      is(node.uri, TEST_URL, "URI of node should match target URL");
+      is(node.uri, url, "URI of node should match target URL");
     }
   }
   else {
     is(query.hasSearchTerms, false,
        "Content tree's searchTerms should not exist after search reset");
     ok(doc.getElementById("searchModifiers").hidden,
        "Scope bar should be hidden after search reset");
   }
@@ -248,58 +275,58 @@ function search(aFolderId, aSearchStr, a
 /**
  * Clicks the given search scope button if it is enabled.
  *
  * @param  aScopeButtonId
  *         the button with this ID will be clicked
  * @return true if the button is enabled, false otherwise
  */
 function selectScope(aScopeButtonId) {
-  var doc = libraryWin.document;
-  var button = doc.getElementById(aScopeButtonId);
+  let doc = gLibrary.document;
+  let button = doc.getElementById(aScopeButtonId);
   isnot(button, null,
-     "Sanity check: scope button with ID " + aScopeButtonId + "should exist");
+     "Sanity check: scope button with ID " + aScopeButtonId + " should exist");
   // Bug 469436 may hide an inappropriate scope button instead of disabling it.
   if (button.disabled || button.hidden)
     return false;
   button.click();
   return true;
 }
 
 /**
  * test() contains window-launching boilerplate that calls this to really kick
  * things off.  Add functions to the testCases array, and this will call them.
- *
- * @param  aLibraryWin
- *         the Places Library window
  */
-function testHelper(aLibraryWin) {
-  libraryWin = aLibraryWin;
+function onLibraryAvailable() {
   testCases.forEach(function (aTest) aTest());
-  aLibraryWin.close();
+
+  gLibrary.close();
+  gLibrary = null;
 
   // Cleanup.
   PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
-  PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.unfiledBookmarksFolder);
-  PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory).removeAllPages();
-
-  finish();
+  PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+  waitForClearHistory(finish);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 
 function test() {
   waitForExplicitFinish();
 
   // Sanity:
   ok(PlacesUtils, "PlacesUtils in context");
-  // Add a visit, a bookmark and a tag.
+
+  // Add visits, a bookmark and a tag.
   PlacesUtils.history.addVisit(PlacesUtils._uri(TEST_URL),
                                Date.now() * 1000, null,
                                PlacesUtils.history.TRANSITION_TYPED, false, 0);
-  PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.unfiledBookmarksFolder,
+  PlacesUtils.history.addVisit(PlacesUtils._uri(TEST_DOWNLOAD_URL),
+                               Date.now() * 1000, null,
+                               PlacesUtils.history.TRANSITION_DOWNLOAD, false, 0);
+  PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                        PlacesUtils._uri(TEST_URL),
                                        PlacesUtils.bookmarks.DEFAULT_INDEX,
                                        "dummy");
   PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
 
-  openLibrary(testHelper);
+  gLibrary = openLibrary(onLibraryAvailable);
 }
--- a/browser/locales/en-US/chrome/browser/places/places.properties
+++ b/browser/locales/en-US/chrome/browser/places/places.properties
@@ -37,16 +37,17 @@ view.sortBy.dateAdded.label=Sort by Adde
 view.sortBy.dateAdded.accesskey=e
 view.sortBy.lastModified.label=Sort by Last Modified
 view.sortBy.lastModified.accesskey=M
 view.sortBy.tags.label=Sort by Tags
 view.sortBy.tags.accesskey=T
 
 searchBookmarks=Search Bookmarks
 searchHistory=Search History
+searchDownloads=Search Downloads
 searchCurrentDefault=Search in '%S'
 findInPrefix=Find in '%S'…
 
 tabs.openWarningTitle=Confirm open
 tabs.openWarningMultipleBranded=You are about to open %S tabs.  This might slow down %S while the pages are loading.  Are you sure you want to continue?
 tabs.openButtonMultiple=Open tabs
 tabs.openWarningPromptMeBranded=Warn me when opening multiple tabs might slow down %S