diff options
Diffstat (limited to 'browser/components/search/content/search.xml')
-rw-r--r-- | browser/components/search/content/search.xml | 2090 |
1 files changed, 2090 insertions, 0 deletions
diff --git a/browser/components/search/content/search.xml b/browser/components/search/content/search.xml new file mode 100644 index 000000000..5c67bc649 --- /dev/null +++ b/browser/components/search/content/search.xml @@ -0,0 +1,2090 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ +<!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd" > +%searchBarDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +]> + +<bindings id="SearchBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="searchbar"> + <resources> + <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/> + <stylesheet src="chrome://browser/skin/searchbar.css"/> + </resources> + <content> + <xul:stringbundle src="chrome://browser/locale/search.properties" + anonid="searchbar-stringbundle"/> + <!-- + There is a dependency between "maxrows" attribute and + "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing + one of them requires changing the other one. + --> + <xul:textbox class="searchbar-textbox" + anonid="searchbar-textbox" + type="autocomplete" + inputtype="search" + flex="1" + autocompletepopup="PopupSearchAutoComplete" + autocompletesearch="search-autocomplete" + autocompletesearchparam="searchbar-history" + maxrows="10" + completeselectedindex="true" + minresultsforpopup="0" + xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines"> + <!-- + Empty <box> to properly position the icon within the autocomplete + binding's anonymous children (the autocomplete binding positions <box> + children differently) + --> + <xul:box> + <xul:hbox class="searchbar-search-button-container"> + <xul:image class="searchbar-search-button" + anonid="searchbar-search-button" + xbl:inherits="addengines" + tooltiptext="&searchEndCap.label;"/> + </xul:hbox> + </xul:box> + <xul:hbox class="search-go-container"> + <xul:image class="search-go-button" hidden="true" + anonid="search-go-button" + onclick="handleSearchCommand(event);" + tooltiptext="&searchEndCap.label;"/> + </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); + + 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)); + ]]></constructor> + + <destructor><![CDATA[ + this.destroy(); + ]]></destructor> + + <method name="destroy"> + <body><![CDATA[ + if (this._initialized) { + this._initialized = false; + + Services.obs.removeObserver(this, "browser-search-engine-modified"); + } + + // Make sure to break the cycle from _textbox to us. Otherwise we leak + // the world. But make sure it's actually pointing to us. + // Also make sure the textbox has ever been constructed, otherwise the + // _textbox getter will cause the textbox constructor to run, add an + // observer, and leak the world too. + if (this._textboxInitialized && this._textbox.mController.input == this) + this._textbox.mController.input = null; + ]]></body> + </method> + + <field name="_ignoreFocus">false</field> + <field name="_clickClosedPopup">false</field> + <field name="_stringBundle">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-stringbundle");</field> + <field name="_textboxInitialized">false</field> + <field name="_textbox">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-textbox");</field> + <field name="_engines">null</field> + <field name="FormHistory" readonly="true"> + (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory; + </field> + + <property name="engines" readonly="true"> + <getter><![CDATA[ + if (!this._engines) + this._engines = Services.search.getVisibleEngines(); + return this._engines; + ]]></getter> + </property> + + <property name="currentEngine"> + <setter><![CDATA[ + Services.search.currentEngine = val; + return val; + ]]></setter> + <getter><![CDATA[ + var currentEngine = Services.search.currentEngine; + // Return a dummy engine if there is no currentEngine + return currentEngine || {name: "", uri: null}; + ]]></getter> + </property> + + <!-- textbox is used by sanitize.js to clear the undo history when + clearing form information. --> + <property name="textbox" readonly="true" + onget="return this._textbox;"/> + + <property name="value" onget="return this._textbox.value;" + onset="return this._textbox.value = val;"/> + + <method name="focus"> + <body><![CDATA[ + this._textbox.focus(); + ]]></body> + </method> + + <method name="select"> + <body><![CDATA[ + this._textbox.select(); + ]]></body> + </method> + + <method name="observe"> + <parameter name="aEngine"/> + <parameter name="aTopic"/> + <parameter name="aVerb"/> + <body><![CDATA[ + if (aTopic == "browser-search-engine-modified") { + switch (aVerb) { + case "engine-removed": + this.offerNewEngine(aEngine); + break; + case "engine-added": + this.hideNewEngine(aEngine); + break; + case "engine-changed": + // An engine was removed (or hidden) or added, or an icon was + // changed. Do nothing special. + } + + // Make sure the engine list is refetched next time it's needed + this._engines = null; + + // Update the popup header and update the display after any modification. + this._textbox.popup.updateHeader(); + this.updateDisplay(); + } + ]]></body> + </method> + + <!-- There are two seaprate lists of search engines, whose uses intersect + in this file. The search service (nsIBrowserSearchService and + nsSearchService.js) maintains a list of Engine objects which is used to + populate the searchbox list of available engines and to perform queries. + That list is accessed here via this.SearchService, and it's that sort of + Engine that is passed to this binding's observer as aEngine. + + In addition, browser.js fills two lists of autodetected search engines + (browser.engines and browser.hiddenEngines) as properties of + mCurrentBrowser. Those lists contain unnamed JS objects of the form + { uri:, title:, icon: }, and that's what the searchbar uses to determine + whether to show any "Add <EngineName>" menu items in the drop-down. + + The two types of engines are currently related by their identifying + titles (the Engine object's 'name'), although that may change; see bug + 335102. --> + + <!-- If the engine that was just removed from the searchbox list was + autodetected on this page, move it to each browser's active list so it + will be offered to be added again. --> + <method name="offerNewEngine"> + <parameter name="aEngine"/> + <body><![CDATA[ + for (let browser of gBrowser.browsers) { + if (browser.hiddenEngines) { + // XXX This will need to be changed when engines are identified by + // URL rather than title; see bug 335102. + var removeTitle = aEngine.wrappedJSObject.name; + for (var i = 0; i < browser.hiddenEngines.length; i++) { + if (browser.hiddenEngines[i].title == removeTitle) { + if (!browser.engines) + browser.engines = []; + browser.engines.push(browser.hiddenEngines[i]); + browser.hiddenEngines.splice(i, 1); + break; + } + } + } + } + BrowserSearch.updateOpenSearchBadge(); + ]]></body> + </method> + + <!-- If the engine that was just added to the searchbox list was + autodetected on this page, move it to each browser's hidden list so it is + no longer offered to be added. --> + <method name="hideNewEngine"> + <parameter name="aEngine"/> + <body><![CDATA[ + for (let browser of gBrowser.browsers) { + if (browser.engines) { + // XXX This will need to be changed when engines are identified by + // URL rather than title; see bug 335102. + var removeTitle = aEngine.wrappedJSObject.name; + for (var i = 0; i < browser.engines.length; i++) { + if (browser.engines[i].title == removeTitle) { + if (!browser.hiddenEngines) + browser.hiddenEngines = []; + browser.hiddenEngines.push(browser.engines[i]); + browser.engines.splice(i, 1); + break; + } + } + } + } + BrowserSearch.updateOpenSearchBadge(); + ]]></body> + </method> + + <method name="setIcon"> + <parameter name="element"/> + <parameter name="uri"/> + <body><![CDATA[ + element.setAttribute("src", uri); + ]]></body> + </method> + + <method name="updateDisplay"> + <body><![CDATA[ + var uri = this.currentEngine.iconURI; + this.setIcon(this, uri ? uri.spec : ""); + + var name = this.currentEngine.name; + var text = this._stringBundle.getFormattedString("searchtip", [name]); + + this._textbox.placeholder = this._stringBundle.getString("searchPlaceholder"); + this._textbox.label = text; + this._textbox.tooltipText = text; + ]]></body> + </method> + + <method name="updateGoButtonVisibility"> + <body><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", + "search-go-button") + .hidden = !this._textbox.value; + ]]></body> + </method> + + <method name="openSuggestionsPanel"> + <parameter name="aShowOnlySettingsIfEmpty"/> + <body><![CDATA[ + if (this._textbox.open) + return; + + this._textbox.showHistoryPopup(); + + if (this._textbox.value) { + // showHistoryPopup does a startSearch("") call, ensure the + // controller handles the text from the input box instead: + this._textbox.mController.handleText(); + } + else if (aShowOnlySettingsIfEmpty) { + this.setAttribute("showonlysettings", "true"); + } + ]]></body> + </method> + + <method name="selectEngine"> + <parameter name="aEvent"/> + <parameter name="isNextEngine"/> + <body><![CDATA[ + // Find the new index + var newIndex = this.engines.indexOf(this.currentEngine); + newIndex += isNextEngine ? 1 : -1; + + if (newIndex >= 0 && newIndex < this.engines.length) { + this.currentEngine = this.engines[newIndex]; + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + + this.openSuggestionsPanel(); + ]]></body> + </method> + + <method name="handleSearchCommand"> + <parameter name="aEvent"/> + <parameter name="aEngine"/> + <parameter name="aForceNewTab"/> + <body><![CDATA[ + var where = "current"; + let params; + + // 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"; + params = { + inBackground: true, + }; + } + } + + this.handleSearchCommandWhere(aEvent, aEngine, where, params); + ]]></body> + </method> + + <method name="handleSearchCommandWhere"> + <parameter name="aEvent"/> + <parameter name="aEngine"/> + <parameter name="aWhere"/> + <parameter name="aParams"/> + <body><![CDATA[ + var textBox = this._textbox; + var textValue = textBox.value; + + let selection = this.telemetrySearchDetails; + let oneOffRecorded = false; + + 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) { + type = "key"; + } else if (aEvent instanceof MouseEvent) { + type = "mouse"; + 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"; + } + } + if (!aEngine) { + aEngine = this.currentEngine; + } + BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type, + aWhere); + } + } + + // This is a one-off search only if oneOffRecorded is true. + this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded); + + if (aWhere == "tab" && aParams && aParams.inBackground) + this.focus(); + ]]></body> + </method> + + <method name="doSearch"> + <parameter name="aData"/> + <parameter name="aWhere"/> + <parameter name="aEngine"/> + <parameter name="aParams"/> + <parameter name="aOneOff"/> + <body><![CDATA[ + var textBox = this._textbox; + + // Save the current value in the form history + if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) { + this.FormHistory.update( + { op : "bump", + fieldname : textBox.getAttribute("autocompletesearchparam"), + value : aData }, + { handleError : function(aError) { + Components.utils.reportError("Saving search to form history failed: " + aError.message); + }}); + } + + let engine = aEngine || this.currentEngine; + var submission = engine.getSubmission(aData, null, "searchbar"); + let telemetrySearchDetails = this.telemetrySearchDetails; + this.telemetrySearchDetails = null; + if (telemetrySearchDetails && telemetrySearchDetails.index == -1) { + telemetrySearchDetails = null; + } + // If we hit here, we come either from a one-off, a plain search or a suggestion. + const details = { + isOneOff: aOneOff, + isSuggestion: (!aOneOff && telemetrySearchDetails), + selection: telemetrySearchDetails + }; + BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details); + // null parameter below specifies HTML response for search + let params = { + postData: submission.postData, + }; + if (aParams) { + for (let key in aParams) { + params[key] = aParams[key]; + } + } + openUILinkIn(submission.uri.spec, aWhere, params); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="command"><![CDATA[ + const target = event.originalTarget; + if (target.engine) { + this.currentEngine = target.engine; + } else if (target.classList.contains("addengine-item")) { + // Select the installed engine if the installation succeeds + var installCallback = { + onSuccess: engine => this.currentEngine = engine + } + Services.search.addEngine(target.getAttribute("uri"), null, + target.getAttribute("src"), false, + installCallback); + } + else + return; + + this.focus(); + this.select(); + ]]></handler> + + <handler event="DOMMouseScroll" + phase="capturing" + modifiers="accel" + action="this.selectEngine(event, (event.detail > 0));"/> + + <handler event="input" action="this.updateGoButtonVisibility();"/> + <handler event="drop" action="this.updateGoButtonVisibility();"/> + + <handler event="blur"> + <![CDATA[ + // If the input field is still focused then a different window has + // received focus, ignore the next focus event. + this._ignoreFocus = (document.activeElement == this._textbox.inputField); + ]]></handler> + + <handler event="focus"> + <![CDATA[ + // Speculatively connect to the current engine's search URI (and + // suggest URI, if different) to reduce request latency + this.currentEngine.speculativeConnect({window: window}); + + if (this._ignoreFocus) { + // This window has been re-focused, don't show the suggestions + this._ignoreFocus = false; + return; + } + + // Don't open the suggestions if there is no text in the textbox. + if (!this._textbox.value) + return; + + // Don't open the suggestions if the mouse was used to focus the + // textbox, that will be taken care of in the click handler. + if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE) + return; + + this.openSuggestionsPanel(); + ]]></handler> + + <handler event="mousedown" phase="capturing"> + <![CDATA[ + if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") { + this._clickClosedPopup = this._textbox.popup._isHiding; + } + ]]></handler> + + <handler event="click" button="0"> + <![CDATA[ + // Ignore clicks on the search go button. + if (event.originalTarget.getAttribute("anonid") == "search-go-button") { + return; + } + + let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button"; + + // Ignore clicks on the icon if they were made to close the popup + if (isIconClick && this._clickClosedPopup) { + return; + } + + // Open the suggestions whenever clicking on the search icon or if there + // is text in the textbox. + if (isIconClick || this._textbox.value) { + this.openSuggestionsPanel(true); + } + ]]></handler> + + </handlers> + </binding> + + <binding id="searchbar-textbox" + extends="chrome://global/content/bindings/autocomplete.xml#autocomplete"> + <implementation implements="nsIObserver"> + <constructor><![CDATA[ + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + if (document.getBindingParent(this).parentNode.parentNode.localName == + "toolbarpaletteitem") + return; + + // Initialize fields + this._stringBundle = document.getBindingParent(this)._stringBundle; + this._suggestEnabled = + Services.prefs.getBoolPref("browser.search.suggest.enabled"); + + if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll")) + this.setAttribute("clickSelectsAll", true); + + // Add items to context menu and attach controller to handle them + var textBox = document.getAnonymousElementByAttribute(this, + "anonid", "textbox-input-box"); + var cxmenu = document.getAnonymousElementByAttribute(textBox, + "anonid", "input-box-contextmenu"); + var pasteAndSearch; + cxmenu.addEventListener("popupshowing", function() { + BrowserSearch.searchBar._textbox.closePopup(); + if (!pasteAndSearch) + return; + var controller = document.commandDispatcher.getControllerForCommand("cmd_paste"); + var enabled = controller.isCommandEnabled("cmd_paste"); + if (enabled) + pasteAndSearch.removeAttribute("disabled"); + else + pasteAndSearch.setAttribute("disabled", "true"); + }, false); + + var element, label, akey; + + element = document.createElementNS(kXULNS, "menuseparator"); + cxmenu.appendChild(element); + + this.setAttribute("aria-owns", this.popup.id); + + var insertLocation = cxmenu.firstChild; + while (insertLocation.nextSibling && + insertLocation.getAttribute("cmd") != "cmd_paste") + insertLocation = insertLocation.nextSibling; + if (insertLocation) { + element = document.createElementNS(kXULNS, "menuitem"); + label = this._stringBundle.getString("cmd_pasteAndSearch"); + element.setAttribute("label", label); + element.setAttribute("anonid", "paste-and-search"); + element.setAttribute("oncommand", "BrowserSearch.pasteAndSearch(event)"); + cxmenu.insertBefore(element, insertLocation.nextSibling); + pasteAndSearch = element; + } + + element = document.createElementNS(kXULNS, "menuitem"); + label = this._stringBundle.getString("cmd_clearHistory"); + akey = this._stringBundle.getString("cmd_clearHistory_accesskey"); + element.setAttribute("label", label); + element.setAttribute("accesskey", akey); + element.setAttribute("cmd", "cmd_clearhistory"); + cxmenu.appendChild(element); + + element = document.createElementNS(kXULNS, "menuitem"); + label = this._stringBundle.getString("cmd_showSuggestions"); + akey = this._stringBundle.getString("cmd_showSuggestions_accesskey"); + element.setAttribute("anonid", "toggle-suggest-item"); + element.setAttribute("label", label); + element.setAttribute("accesskey", akey); + element.setAttribute("cmd", "cmd_togglesuggest"); + element.setAttribute("type", "checkbox"); + element.setAttribute("checked", this._suggestEnabled); + element.setAttribute("autocheck", "false"); + this._suggestMenuItem = element; + cxmenu.appendChild(element); + + this.addEventListener("keypress", aEvent => { + 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); + ]]></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 { + this.controllers.removeController(this.searchbarController); + } catch (ex) { } + ]]></destructor> + + <field name="_stringBundle"/> + <field name="_suggestMenuItem"/> + <field name="_suggestEnabled"/> + + <!-- + This overrides the searchParam property in autocomplete.xml. We're + hijacking this property as a vehicle for delivering the privacy + information about the window into the guts of nsSearchSuggestions. + + Note that the setter is the same as the parent. We were not sure whether + we can override just the getter. If that proves to be the case, the setter + can be removed. + --> + <property name="searchParam" + onget="return this.getAttribute('autocompletesearchparam') + + (PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');" + onset="this.setAttribute('autocompletesearchparam', val); return val;"/> + + <!-- This is implemented so that when textbox.value is set directly (e.g., + by tests), the one-off query is updated. --> + <method name="onBeforeValueSet"> + <parameter name="aValue"/> + <body><![CDATA[ + this.popup.oneOffButtons.query = aValue; + return aValue; + ]]></body> + </method> + + <!-- + This method overrides the autocomplete binding's openPopup (essentially + duplicating the logic from the autocomplete popup binding's + openAutocompletePopup method), modifying it so that the popup is aligned with + the inner textbox, but sized to not extend beyond the search bar border. + --> + <method name="openPopup"> + <body><![CDATA[ + var popup = this.popup; + if (!popup.mPopupOpen) { + // Initially the panel used for the searchbar (PopupSearchAutoComplete + // in browser.xul) is hidden to avoid impacting startup / new + // window performance. The base binding's openPopup would normally + // call the overriden openAutocompletePopup in urlbarBindings.xml's + // browser-autocomplete-result-popup binding to unhide the popup, + // but since we're overriding openPopup we need to unhide the panel + // ourselves. + popup.hidden = false; + + // Don't roll up on mouse click in the anchor for the search UI. + if (popup.id == "PopupSearchAutoComplete") { + popup.setAttribute("norolluponanchor", "true"); + } + + popup.mInput = this; + popup.view = this.controller.QueryInterface(Ci.nsITreeView); + popup.invalidate(); + + popup.showCommentColumn = this.showCommentColumn; + popup.showImageColumn = this.showImageColumn; + + document.popupNode = null; + + const isRTL = getComputedStyle(this, "").direction == "rtl"; + + var outerRect = this.getBoundingClientRect(); + var innerRect = this.inputField.getBoundingClientRect(); + let width = isRTL ? + innerRect.right - outerRect.left : + outerRect.right - innerRect.left; + popup.setAttribute("width", width > 100 ? width : 100); + + var yOffset = outerRect.bottom - innerRect.bottom; + popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false); + } + ]]></body> + </method> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + if (aTopic == "nsPref:changed") { + this._suggestEnabled = + Services.prefs.getBoolPref("browser.search.suggest.enabled"); + this._suggestMenuItem.setAttribute("checked", this._suggestEnabled); + } + ]]></body> + </method> + + <method name="openSearch"> + <body> + <![CDATA[ + if (!this.popupOpen) { + document.getBindingParent(this).openSuggestionsPanel(); + return false; + } + return true; + ]]> + </body> + </method> + + <!-- override |onTextEntered| in autocomplete.xml --> + <method name="onTextEntered"> + <parameter name="aEvent"/> + <body><![CDATA[ + let engine; + let oneOff = this.selectedButton; + if (oneOff) { + if (!oneOff.engine) { + oneOff.doCommand(); + return; + } + engine = oneOff.engine; + } + if (this._selectionDetails && + this._selectionDetails.currentIndex != -1) { + BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails; + this._selectionDetails = null; + } + document.getBindingParent(this).handleSearchCommand(aEvent, engine); + ]]></body> + </method> + + <property name="selectedButton"> + <getter><![CDATA[ + return this.popup.oneOffButtons.selectedButton; + ]]></getter> + <setter><![CDATA[ + return this.popup.oneOffButtons.selectedButton = val; + ]]></setter> + </property> + + <method name="handleKeyboardNavigation"> + <parameter name="aEvent"/> + <body><![CDATA[ + let popup = this.popup; + 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"; + 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, + supportsCommand: function(aCommand) { + return aCommand == "cmd_clearhistory" || + aCommand == "cmd_togglesuggest"; + }, + + isCommandEnabled: function(aCommand) { + return true; + }, + + doCommand: function (aCommand) { + switch (aCommand) { + case "cmd_clearhistory": + var param = this._self.getAttribute("autocompletesearchparam"); + + BrowserSearch.searchBar.FormHistory.update({ op : "remove", fieldname : param }, null); + this._self.value = ""; + break; + case "cmd_togglesuggest": + // The pref observer will update _suggestEnabled and the menu + // checkmark. + Services.prefs.setBoolPref("browser.search.suggest.enabled", + !this._self._suggestEnabled); + break; + default: + // do nothing with unrecognized command + } + } + })]]></field> + </implementation> + + <handlers> + <handler event="input"><![CDATA[ + this.popup.removeAttribute("showonlysettings"); + ]]></handler> + + <handler event="keypress" phase="capturing" + action="return this.handleKeyboardNavigation(event);"/> + + <handler event="keypress" keycode="VK_UP" modifiers="accel" + phase="capturing" + action="document.getBindingParent(this).selectEngine(event, false);"/> + + <handler event="keypress" keycode="VK_DOWN" modifiers="accel" + phase="capturing" + action="document.getBindingParent(this).selectEngine(event, true);"/> + + <handler event="keypress" keycode="VK_DOWN" modifiers="alt" + phase="capturing" + action="return this.openSearch();"/> + + <handler event="keypress" keycode="VK_UP" modifiers="alt" + phase="capturing" + action="return this.openSearch();"/> + + <handler event="dragover"> + <![CDATA[ + var types = event.dataTransfer.types; + if (types.includes("text/plain") || types.includes("text/x-moz-text-internal")) + event.preventDefault(); + ]]> + </handler> + + <handler event="drop"> + <![CDATA[ + var dataTransfer = event.dataTransfer; + var data = dataTransfer.getData("text/plain"); + if (!data) + data = dataTransfer.getData("text/x-moz-text-internal"); + if (data) { + event.preventDefault(); + this.value = data; + document.getBindingParent(this).openSuggestionsPanel(); + } + ]]> + </handler> + + </handlers> + </binding> + + <binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup"> + <resources> + <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/> + <stylesheet src="chrome://browser/skin/searchbar.css"/> + </resources> + <content ignorekeys="true" level="top" consumeoutsideclicks="never"> + <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" + 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:tree> + <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/> + </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> + <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); + } + return this._bundle; + ]]> + </getter> + </property> + + <field name="oneOffButtons" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", + "search-one-off-buttons"); + </field> + + <method name="updateHeader"> + <body><![CDATA[ + let currentEngine = Services.search.currentEngine; + let uri = currentEngine.iconURI; + if (uri) { + this.setAttribute("src", uri.spec); + } + else { + // If the default has just been changed to a provider without icon, + // avoid showing the icon of the previous default provider. + this.removeAttribute("src"); + } + + let headerText = this.bundle.formatStringFromName("searchHeader", + [currentEngine.name], 1); + document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name") + .setAttribute("value", headerText); + document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine") + .engine = currentEngine; + ]]></body> + </method> + + <!-- This is called when a one-off is clicked and when "search in new tab" + is selected from a one-off context menu. --> + <method name="handleOneOffSearch"> + <parameter name="event"/> + <parameter name="engine"/> + <parameter name="where"/> + <parameter name="params"/> + <body><![CDATA[ + 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"; + + // 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; + } + + // 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; + } + 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; + } + + // Show the current default engine in the top header of the panel. + this.updateHeader(); + ]]></handler> + + <handler event="popuphiding"><![CDATA[ + this._isHiding = true; + setTimeout(() => { + this._isHiding = false; + }, 0); + ]]></handler> + + <!-- This handles clicks on the topmost "Foo Search" header in the + popup (hbox[anonid="searchbar-engine"]). --> + <handler event="click"><![CDATA[ + if (event.button == 2) { + // Ignore right clicks. + return; + } + let button = event.originalTarget; + let engine = button.parentNode.engine; + if (!engine) { + return; + } + this.oneOffButtons.handleSearchCommand(event, engine); + ]]></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> + + <binding id="search-one-offs"> + <content context="_child"> + <xul:deck anonid="search-panel-one-offs-header" + selectedIndex="0" + class="search-panel-header search-panel-current-input"> + <xul:label anonid="searchbar-oneoffheader-search" + value="&searchWithHeader.label;"/> + <xul:hbox anonid="search-panel-searchforwith" + class="search-panel-current-input"> + <xul:label anonid="searchbar-oneoffheader-before" + value="&searchFor.label;"/> + <xul:label anonid="searchbar-oneoffheader-searchtext" + class="search-panel-input-value" + flex="1" + crop="end"/> + <xul:label anonid="searchbar-oneoffheader-after" + flex="10000" + value="&searchWith.label;"/> + </xul:hbox> + <xul:hbox anonid="search-panel-searchonengine" + class="search-panel-current-input"> + <xul:label anonid="searchbar-oneoffheader-beforeengine" + value="&search.label;"/> + <xul:label anonid="searchbar-oneoffheader-engine" + class="search-panel-input-value" + flex="1" + crop="end"/> + <xul:label anonid="searchbar-oneoffheader-afterengine" + flex="10000" + value="&searchAfter.label;"/> + </xul:hbox> + </xul:deck> + <xul:description anonid="search-panel-one-offs" + role="group" + 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: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"> + + <!-- 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); + } + } + if (val) { + for (let event of events) { + val.addEventListener(event, this); + } + } + this._popup = val; + + // If the popup is already open, rebuild the one-offs now. The + // popup may be opening, so check that the state is not closed + // instead of checking popupOpen. + if (val && val.state != "closed") { + this._rebuild(); + } + return val; + ]]></setter> + </property> + + <field name="_textbox">null</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> + </property> + + <!-- Set this to a string that identifies your one-offs consumer. It'll + be appended to telemetry recorded with maybeRecordTelemetry(). --> + <field name="telemetryOrigin">""</field> + + <field name="_query">""</field> + + <!-- The query string currently shown in the one-offs. If the textbox + property is non-null, then this is automatically updated on + input. --> + <property name="query"> + <getter><![CDATA[ + return this._query; + ]]></getter> + <setter><![CDATA[ + this._query = val; + if (this.popup && this.popup.popupOpen) { + this._updateAfterQueryChanged(); + } + return val; + ]]></setter> + </property> + + <field name="_selectedButton">null</field> + + <!-- 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); + 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[ + let buttons = this.getSelectableButtons(true); + for (let i = 0; i < buttons.length; i++) { + if (buttons[i] == this._selectedButton) { + return i; + } + } + return -1; + ]]></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="_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); + } + return this._bundle; + ]]></getter> + </property> + + <!-- 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> + + <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> + + <!-- 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) { + case "input": + // Allow the consumer's input to override its value property with + // a oneOffSearchQuery property. That way if the value is not + // 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(() => { + this.selectedButton = null; + this._contextEngine = null; + }, Ci.nsIThread.DISPATCH_NORMAL); + break; + } + ]]></body> + </method> + + <method name="showSettings"> + <body><![CDATA[ + BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin); + openPreferences("paneSearch"); + // 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; + } + else { + let noSearchHeader = + document.getAnonymousElementByAttribute(this, "anonid", + "searchbar-oneoffheader-search"); + groupText = noSearchHeader.value; + if (!isOneOffSelected) + headerPanel.selectedIndex = 0; + } + list.setAttribute("aria-label", groupText); + ]]></body> + </method> + + <!-- 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. + let addEngineList = + document.getAnonymousElementByAttribute(this, "anonid", "add-engines"); + while (addEngineList.firstChild) + addEngineList.firstChild.remove(); + + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + // Add a button for each engine that the page in the selected browser + // offers. But 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 site offers, it + // could effectively break the urlbar popup by offering a ton of + // engines. We should probably make a smaller version of the buttons + // for compact one-offs. + if (!this.compact) { + for (let engine of gBrowser.selectedBrowser.engines || []) { + let button = document.createElementNS(kXULNS, "button"); + let label = this.bundle.formatStringFromName("cmd_addFoundEngine", + [engine.title], 1); + button.id = this.telemetryOrigin + "-add-engine-" + + engine.title.replace(/ /g, '-'); + button.setAttribute("class", "addengine-item"); + button.setAttribute("label", label); + button.setAttribute("pack", "start"); + + button.setAttribute("crop", "end"); + button.setAttribute("tooltiptext", engine.uri); + button.setAttribute("uri", engine.uri); + if (engine.icon) { + button.setAttribute("image", engine.icon); + } + button.setAttribute("title", engine.title); + addEngineList.appendChild(button); + } + } + + let settingsButton = + document.getAnonymousElementByAttribute(this, "anonid", + "search-settings-compact"); + // Finally, build the list of one-off buttons. + while (list.firstChild != settingsButton) + list.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(",") : []; + + 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 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; + + if (!engines.length) + 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"); + + // 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 dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow); + for (let i = 0; i < engines.length; ++i) { + let engine = engines[i]; + let button = document.createElementNS(kXULNS, "button"); + button.id = this._buttonIDForEngine(engine); + let uri = "chrome://browser/skin/search-engine-placeholder.png"; + if (engine.iconURI) { + uri = engine.iconURI.spec; + } + button.setAttribute("image", uri); + button.setAttribute("class", "searchbar-engine-one-off-item"); + button.setAttribute("tooltiptext", engine.name); + button.setAttribute("width", buttonWidth); + button.engine = engine; + + if ((i + 1) % enginesPerRow == 0) + 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); + } + + 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); + } + + if (this.compact) { + this.settingsButton.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 + // aren't an integral multiple of device pixels, the calculated + // remainder can end up being ~1px too big, at least on Windows, + // which pushes the settings button to a new row. The remainder + // is integral, not a fraction, so that's not the problem. To + // work around that, unscale the remainder, floor it, scale it + // back, and then floor that. + let scale = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .screenPixelsPerCSSPixel; + let remainder = panelWidth - (enginesPerRow * buttonWidth); + remainder = Math.floor(Math.floor(remainder * scale) / scale); + let width = remainder + buttonWidth; + let lastDummyItem = this.settingsButton.previousSibling; + lastDummyItem.setAttribute("width", width); + } + } + ]]></body> + </method> + + <method name="_buttonIDForEngine"> + <parameter name="engine"/> + <body><![CDATA[ + return this.telemetryOrigin + "-engine-one-off-item-" + + engine.name.replace(/ /g, '-'); + ]]></body> + </method> + + <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"/> + <body><![CDATA[ + let visuallySelectedButton = this.visuallySelectedButton; + if (visuallySelectedButton) + visuallySelectedButton.removeAttribute("selected"); + + 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; + if (this.textbox) { + this.textbox.removeAttribute("aria-activedescendant"); + } + } + + 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); + } + ]]></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) { + // 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; + + 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"/> + <body><![CDATA[ + let where = "current"; + let params; + + // Open ctrl/cmd clicks on one-off buttons in a new background tab. + if (aForceNewTab) { + where = "tab"; + if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) { + params = { + inBackground: true, + }; + } + } + 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"; + params = { + inBackground: true, + }; + } + } + + this.popup.handleOneOffSearch(aEvent, aEngine, where, params); + ]]></body> + </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 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. + @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="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; + } + + // 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; + ]]></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 + 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. + + @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 + Pass true if it's OK that neither the list nor the one-off + 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; + } + + let stopEvent = false; + + // 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); + } + + // 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. + else if (event.altKey && + (event.keyCode == KeyEvent.DOM_VK_DOWN || + event.keyCode == KeyEvent.DOM_VK_UP)) { + 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); + } + } + + if (stopEvent) { + event.preventDefault(); + event.stopPropagation(); + return true; + } + 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. + + @param aEvent + An event, like a click on a one-off button. + @param aOpenUILinkWhere + The "where" passed to openUILink. + @param aOpenUILinkParams + The "params" passed to openUILink. + @return True if telemetry was recorded and false if not. + --> + <method name="maybeRecordTelemetry"> + <parameter name="aEvent"/> + <parameter name="aOpenUILinkWhere"/> + <parameter name="aOpenUILinkParams"/> + <body><![CDATA[ + if (!aEvent) { + return false; + } + + let source = null; + let type = "unknown"; + let engine = null; + let target = aEvent.originalTarget; + + if (aEvent instanceof KeyboardEvent) { + type = "key"; + if (this.selectedButton) { + source = "oneoff"; + engine = this.selectedButton.engine; + } + } else if (aEvent instanceof MouseEvent) { + type = "mouse"; + if (target.classList.contains("searchbar-engine-one-off-item")) { + source = "oneoff"; + engine = target.engine; + } + } else if ((aEvent instanceof XULCommandEvent) && + target.getAttribute("anonid") == + "search-one-offs-context-open-in-new-tab") { + source = "oneoff-context"; + engine = this._contextEngine; + } + + if (!source) { + return false; + } + + if (this.telemetryOrigin) { + source += "-" + this.telemetryOrigin; + } + + let tabBackground = aOpenUILinkWhere == "tab" && + aOpenUILinkParams && + aOpenUILinkParams.inBackground; + let where = tabBackground ? "tab-background" : aOpenUILinkWhere; + BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type, + where); + return true; + ]]></body> + </method> + + </implementation> + + <handlers> + + <handler event="mousedown"><![CDATA[ + // Required to receive click events from the buttons on Linux. + event.preventDefault(); + ]]></handler> + + <handler event="mousemove"><![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")) { + this._changeVisuallySelectedButton(target); + } + ]]></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; + + // 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); + } + ]]></handler> + + <handler event="click"><![CDATA[ + if (event.button == 2) + return; // ignore right clicks. + + let button = event.originalTarget; + let engine = button.engine; + + if (!engine) + return; + + // Select the clicked button so that consumers can easily tell which + // button was acted on. + this.selectedButton = button; + this.handleSearchCommand(event, engine); + ]]></handler> + + <handler event="command"><![CDATA[ + let target = event.originalTarget; + if (target.classList.contains("addengine-item")) { + // On success, hide the panel and tell event listeners to reshow it to + // show the new engine. + let installCallback = { + onSuccess: engine => { + this._rebuild(); + }, + onError: function(errorCode) { + if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) { + // Download error is shown by the search service + return; + } + const kSearchBundleURI = "chrome://global/locale/search/search.properties"; + let searchBundle = Services.strings.createBundle(kSearchBundleURI); + let brandBundle = document.getElementById("bundle_brand"); + let brandName = brandBundle.getString("brandShortName"); + let title = searchBundle.GetStringFromName("error_invalid_engine_title"); + let text = searchBundle.formatStringFromName("error_duplicate_engine_msg", + [brandName, target.getAttribute("uri")], 2); + Services.prompt.QueryInterface(Ci.nsIPromptFactory); + let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt); + prompt.QueryInterface(Ci.nsIWritablePropertyBag2); + prompt.setPropertyAsBool("allowTabModal", true); + prompt.alert(title, text); + } + } + Services.search.addEngine(target.getAttribute("uri"), null, + target.getAttribute("image"), false, + installCallback); + } + let anonid = target.getAttribute("anonid"); + if (anonid == "search-one-offs-context-open-in-new-tab") { + // Select the context-clicked button so that consumers can easily + // tell which button was acted on. + this.selectedButton = this._buttonForEngine(this._contextEngine); + this.handleSearchCommand(event, this._contextEngine, true); + } + if (anonid == "search-one-offs-context-set-default") { + let currentEngine = Services.search.currentEngine; + + if (!this.getAttribute("includecurrentengine")) { + // 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 = this._buttonForEngine(this._contextEngine); + button.id = this._buttonIDForEngine(currentEngine); + let uri = "chrome://browser/skin/search-engine-placeholder.png"; + if (currentEngine.iconURI) + uri = currentEngine.iconURI.spec; + button.setAttribute("image", uri); + button.setAttribute("tooltiptext", currentEngine.name); + button.engine = currentEngine; + } + + Services.search.currentEngine = this._contextEngine; + } + ]]></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; + } + document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default") + .setAttribute("disabled", target.engine == Services.search.currentEngine); + + this._contextEngine = target.engine; + ]]></handler> + </handlers> + + </binding> + +</bindings> |