Bug 1110771 - One-click search buttons should have a right-click menu. r=florian
authorNihanth Subramanya <nhnt11@.gmail.com>
Fri, 18 Sep 2015 16:51:23 -0700
changeset 295962 e2df07d3f7eeed73375efe23aca5f56335a575c4
parent 295961 372489a1e361974635ce4cf31e03bfec0f6a6df5
child 295963 4313752f69956ae248bd4e7ff3913c8dd4252698
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian
bugs1110771
milestone43.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 1110771 - One-click search buttons should have a right-click menu. r=florian
browser/components/search/content/search.xml
browser/locales/en-US/chrome/browser/browser.dtd
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -330,28 +330,34 @@
 
           this.openSuggestionsPanel();
         ]]></body>
       </method>
 
       <method name="handleSearchCommand">
         <parameter name="aEvent"/>
         <parameter name="aEngine"/>
+        <parameter name="aForceNewTab"/>
         <body><![CDATA[
           var textBox = this._textbox;
           var textValue = textBox.value;
 
           var where = "current";
 
           // Open ctrl/cmd clicks on one-off buttons in a new background tab.
           if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
             if (aEvent.button == 2)
               return;
             where = whereToOpenLink(aEvent, false, true);
           }
+          else if (aForceNewTab) {
+            where = "tab";
+            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
+              where += "-background";
+          }
           else {
             var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
             if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
               where = "tab";
             if ((aEvent instanceof MouseEvent) &&
                 (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
               where = "tab-background";
             }
@@ -375,16 +381,18 @@
                 source = "oneoff";
               } else if (target.classList.contains("search-panel-header") ||
                          target.parentNode.classList.contains("search-panel-header")) {
                 source = "header";
               }
             } else if (aEvent instanceof XULCommandEvent) {
               if (target.getAttribute("anonid") == "paste-and-search") {
                 source = "paste";
+              } else if (target.getAttribute("anonid") == "search-one-offs-context-open-in-new-tab") {
+                source = "oneoff-context";
               }
             }
 
             if (!aEngine) {
               aEngine = this.currentEngine;
             }
             BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type, where);
           }
@@ -1002,17 +1010,17 @@
 
     </handlers>
   </binding>
 
   <binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup">
     <resources>
       <stylesheet src="chrome://browser/skin/searchbar.css"/>
     </resources>
-    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+    <content ignorekeys="true" level="top" consumeoutsideclicks="never" context="_child">
       <xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
                 class="search-panel-header search-panel-current-engine">
         <xul:image class="searchbar-engine-image" xbl:inherits="src"/>
         <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
                    role="presentation"/>
       </xul:hbox>
       <xul:tree anonid="tree" flex="1"
                 class="autocomplete-tree plain search-panel-tree"
@@ -1044,23 +1052,34 @@
       <xul:description anonid="search-panel-one-offs"
                        role="group"
                        class="search-panel-one-offs"/>
       <xul:vbox anonid="add-engines"/>
       <xul:button anonid="search-settings"
                   oncommand="showSettings();"
                   class="search-setting-button search-panel-header"
                   label="&changeSearchSettings.button;"/>
+      <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>
       <!-- Popup rollup is triggered by native events before the mousedown event
            reaches the DOM. The will be set to true by the popuphiding event and
            false after the mousedown event has been triggered to detect what
            caused rollup. -->
       <field name="_isHiding">false</field>
+      <!-- When a context menu is opened on a one-off button, this is set to the
+           engine of that button for use with the context menu actions. -->
+      <field name="_contextEngine">null</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);
             }
@@ -1096,22 +1115,51 @@
         <body><![CDATA[
           BrowserUITelemetry.countSearchSettingsEvent("searchbar");
           openPreferences("paneSearch");
           // If the preference tab was already selected, the panel doesn't
           // close itself automatically.
           BrowserSearch.searchBar._textbox.closePopup();
         ]]></body>
       </method>
+
+      <constructor><![CDATA[
+        // Prevent popup events from the context menu from reaching the autocomplete
+        // binding (or other listeners).
+        let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
+        let listener = aEvent => aEvent.stopPropagation();
+        menu.addEventListener("popupshowing", listener);
+        menu.addEventListener("popuphiding", listener);
+        menu.addEventListener("popupshown", aEvent => {
+          this._ignoreMouseEvents = true;
+          aEvent.stopPropagation();
+        });
+        menu.addEventListener("popuphidden", aEvent => {
+          this._ignoreMouseEvents = false;
+          aEvent.stopPropagation();
+        });
+      ]]></constructor>
     </implementation>
     <handlers>
       <handler event="popuphidden"><![CDATA[
         Services.tm.mainThread.dispatch(function() {
           document.getElementById("searchbar").textbox.selectedButton = null;
         }, Ci.nsIThread.DISPATCH_NORMAL);
+        this._contextEngine = null;
+      ]]></handler>
+
+      <handler event="contextmenu"><![CDATA[
+        let target = event.originalTarget;
+        // Prevent the context menu from appearing except on the one off buttons.
+        if (!target.classList.contains("searchbar-engine-one-off-item") ||
+            target.classList.contains("dummy")) {
+          event.preventDefault();
+          return;
+        }
+        this._contextEngine = target.engine;
       ]]></handler>
 
       <handler event="popupshowing"><![CDATA[
         // 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",
@@ -1316,46 +1364,62 @@
         event.preventDefault();
       ]]></handler>
 
       <handler event="mouseover"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button")
           return;
 
+        // Ignore mouse events when the context menu is open.
+         if (this._ignoreMouseEvents)
+           return;
+
         if ((target.classList.contains("searchbar-engine-one-off-item") &&
              !target.classList.contains("dummy")) ||
             target.classList.contains("addengine-item") ||
             target.classList.contains("search-setting-button")) {
           let textbox = document.getElementById("searchbar").textbox;
           textbox.selectedButton = target;
           textbox.selectionFromMouseOver = true;
         }
       ]]></handler>
 
       <handler event="mouseout"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button")
           return;
 
+        // Don't deselect the current button if the context menu is open.
+        if (this._ignoreMouseEvents)
+          return;
+
         let textbox = document.getElementById("searchbar").textbox;
         if (textbox.selectedButton == target)
           textbox.selectedButton = null;
       ]]></handler>
 
       <handler event="click"><![CDATA[
         if (event.button == 2)
           return; // ignore right clicks.
 
         let button = event.originalTarget;
         let engine = button.engine || button.parentNode.engine;
 
         if (!engine)
           return;
 
+        // For some reason, if the context menu had been opened prior to the
+        // click, the suggestions popup won't be closed after loading the search
+        // in the current tab - so we hide it manually. Some focusing magic
+        // that happens when a search is loaded ensures that the popup is opened
+        // again if it needs to be, so we don't need to worry about which cases
+        // require manual hiding.
+        this.hidePopup();
+
         let searchbar = document.getElementById("searchbar");
         searchbar.handleSearchCommand(event, engine);
       ]]></handler>
 
       <handler event="command"><![CDATA[
         let target = event.originalTarget;
         if (target.classList.contains("addengine-item")) {
           // On success, hide and reshow the panel to show the new engine.
@@ -1368,16 +1432,39 @@
               Components.utils.reportError("Error adding search engine: " + errorCode);
             }
           }
           Services.search.addEngine(target.getAttribute("uri"),
                                     Ci.nsISearchEngine.DATA_XML,
                                     target.getAttribute("image"), false,
                                     installCallback);
         }
+        let anonid = target.getAttribute("anonid");
+        if (anonid == "search-one-offs-context-open-in-new-tab") {
+          let searchbar = document.getElementById("searchbar");
+          searchbar.handleSearchCommand(event, this._contextEngine, true);
+        }
+        if (anonid == "search-one-offs-context-set-default") {
+          let currentEngine = Services.search.currentEngine;
+
+          // Make the target button of the context menu reflect the current
+          // search engine first. Doing this as opposed to rebuilding all the
+          // one-off buttons avoids flicker.
+          let button = document.getElementById("searchbar-engine-one-off-item-" +
+            this._contextEngine.name.replace(/ /g, '-'));
+          button.id = "searchbar-engine-one-off-item-" + currentEngine.name.replace(/ /g, '-');
+          let uri = "chrome://browser/skin/search-engine-placeholder.png";
+          if (currentEngine.iconURI)
+            uri = PlacesUtils.getImageURLForResolution(window, currentEngine.iconURI.spec);
+          button.setAttribute("image", uri);
+          button.setAttribute("tooltiptext", currentEngine.name);
+          button.engine = currentEngine;
+
+          Services.search.currentEngine = this._contextEngine;
+        }
       ]]></handler>
 
       <handler event="popuphiding"><![CDATA[
         this._isHiding = true;
         setTimeout(() => {
           this._isHiding = false;
         }, 0);
       ]]></handler>
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -466,16 +466,21 @@ These should match what Safari and other
      searchFor.label and searchWith.label. This string will be used instead of
      them when the user has not typed any keyword. -->
 <!ENTITY searchWithHeader.label       "Search with:">
 <!-- LOCALIZATION NOTE (changeSearchSettings.button):
      This string won't wrap, so if the translated string is longer,
      consider translating it as if it said only "Search Settings". -->
 <!ENTITY changeSearchSettings.button  "Change Search Settings">
 
+<!ENTITY searchInNewTab.label         "Search in New Tab">
+<!ENTITY searchInNewTab.accesskey     "T">
+<!ENTITY searchSetAsDefault.label     "Set As Default Search Engine">
+<!ENTITY searchSetAsDefault.accesskey "D">
+
 <!ENTITY tabView.commandkey           "e">
 
 <!ENTITY openLinkCmdInTab.label       "Open Link in New Tab">
 <!ENTITY openLinkCmdInTab.accesskey   "T">
 <!ENTITY openLinkCmd.label            "Open Link in New Window">
 <!ENTITY openLinkCmd.accesskey        "W">
 <!ENTITY openLinkInPrivateWindowCmd.label "Open Link in New Private Window">
 <!ENTITY openLinkInPrivateWindowCmd.accesskey "P">