diff options
Diffstat (limited to 'browser/components/search')
-rw-r--r-- | browser/components/search/content/engineManager.js | 492 | ||||
-rw-r--r-- | browser/components/search/content/engineManager.xul | 93 | ||||
-rw-r--r-- | browser/components/search/content/search.xml | 834 | ||||
-rw-r--r-- | browser/components/search/content/searchbarBindings.css | 13 | ||||
-rw-r--r-- | browser/components/search/jar.mn | 9 | ||||
-rw-r--r-- | browser/components/search/moz.build | 6 |
6 files changed, 1447 insertions, 0 deletions
diff --git a/browser/components/search/content/engineManager.js b/browser/components/search/content/engineManager.js new file mode 100644 index 000000000..b9ed17cbc --- /dev/null +++ b/browser/components/search/content/engineManager.js @@ -0,0 +1,492 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const Ci = Components.interfaces; +const Cc = Components.classes; + +const ENGINE_FLAVOR = "text/x-moz-search-engine"; + +const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled"; + +var gEngineView = null; + +var gEngineManagerDialog = { + init: function() { + gEngineView = new EngineView(new EngineStore()); + + var suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF); + document.getElementById("enableSuggest").checked = suggestEnabled; + + var tree = document.getElementById("engineList"); + tree.view = gEngineView; + + Services.obs.addObserver(this, "browser-search-engine-modified", false); + }, + + destroy: function() { + // Remove the observer + Services.obs.removeObserver(this, "browser-search-engine-modified"); + }, + + observe: function(aEngine, aTopic, aVerb) { + if (aTopic == "browser-search-engine-modified") { + aEngine.QueryInterface(Ci.nsISearchEngine); + switch (aVerb) { + case "engine-added": + gEngineView._engineStore.addEngine(aEngine); + gEngineView.rowCountChanged(gEngineView.lastIndex, 1); + break; + case "engine-changed": + gEngineView._engineStore.reloadIcons(); + gEngineView.invalidate(); + break; + case "engine-removed": + case "engine-current": + case "engine-default": + // Not relevant + break; + } + } + }, + + onOK: function() { + // Set the preference + var newSuggestEnabled = document.getElementById("enableSuggest").checked; + Services.prefs.setBoolPref(BROWSER_SUGGEST_PREF, newSuggestEnabled); + + // Commit the changes + gEngineView._engineStore.commit(); + }, + + onRestoreDefaults: function() { + var num = gEngineView._engineStore.restoreDefaultEngines(); + gEngineView.rowCountChanged(0, num); + gEngineView.invalidate(); + }, + + showRestoreDefaults: function(val) { + document.documentElement.getButton("extra2").disabled = !val; + }, + + loadAddEngines: function() { + this.onOK(); + window.opener.BrowserSearch.loadAddEngines(); + window.close(); + }, + + remove: function() { + gEngineView._engineStore.removeEngine(gEngineView.selectedEngine); + var index = gEngineView.selectedIndex; + gEngineView.rowCountChanged(index, -1); + gEngineView.invalidate(); + gEngineView.selection.select(Math.min(index, gEngineView.lastIndex)); + gEngineView.ensureRowIsVisible(gEngineView.currentIndex); + document.getElementById("engineList").focus(); + }, + + /** + * Moves the selected engine either up or down in the engine list + * @param aDir + * -1 to move the selected engine down, +1 to move it up. + */ + bump: function(aDir) { + var selectedEngine = gEngineView.selectedEngine; + var newIndex = gEngineView.selectedIndex - aDir; + + gEngineView._engineStore.moveEngine(selectedEngine, newIndex); + + gEngineView.invalidate(); + gEngineView.selection.select(newIndex); + gEngineView.ensureRowIsVisible(newIndex); + this.showRestoreDefaults(true); + document.getElementById("engineList").focus(); + }, + + editKeyword: Task.async(function* () { + var selectedEngine = gEngineView.selectedEngine; + if (!selectedEngine) + return; + + var alias = { value: selectedEngine.alias }; + var strings = document.getElementById("engineManagerBundle"); + var title = strings.getString("editTitle"); + var msg = strings.getFormattedString("editMsg", [selectedEngine.name]); + + while (Services.prompt.prompt(window, title, msg, alias, null, {})) { + var bduplicate = false; + var eduplicate = false; + var dupName = ""; + + if (alias.value != "") { + // Check for duplicates in Places keywords. + bduplicate = !!(yield PlacesUtils.keywords.fetch(alias.value)); + + // Check for duplicates in changes we haven't committed yet + let engines = gEngineView._engineStore.engines; + for each (let engine in engines) { + if (engine.alias == alias.value && + engine.name != selectedEngine.name) { + eduplicate = true; + dupName = engine.name; + break; + } + } + } + + // Notify the user if they have chosen an existing engine/bookmark keyword + if (eduplicate || bduplicate) { + var dtitle = strings.getString("duplicateTitle"); + var bmsg = strings.getString("duplicateBookmarkMsg"); + var emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]); + + Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg); + } else { + gEngineView._engineStore.changeEngine(selectedEngine, "alias", + alias.value); + gEngineView.invalidate(); + break; + } + } + }), + + onSelect: function() { + // Buttons only work if an engine is selected and it's not the last engine, + // the latter is true when the selected is first and last at the same time. + var lastSelected = (gEngineView.selectedIndex == gEngineView.lastIndex); + var firstSelected = (gEngineView.selectedIndex == 0); + var noSelection = (gEngineView.selectedIndex == -1); + + document.getElementById("cmd_remove") + .setAttribute("disabled", noSelection || + (firstSelected && lastSelected)); + + document.getElementById("cmd_moveup") + .setAttribute("disabled", noSelection || firstSelected); + + document.getElementById("cmd_movedown") + .setAttribute("disabled", noSelection || lastSelected); + + document.getElementById("cmd_editkeyword") + .setAttribute("disabled", noSelection); + } +}; + +function onDragEngineStart(event) { + var selectedIndex = gEngineView.selectedIndex; + if (selectedIndex >= 0) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } +} + +// "Operation" objects +function EngineMoveOp(aEngineClone, aNewIndex) { + if (!aEngineClone) + throw new Error("bad args to new EngineMoveOp!"); + this._engine = aEngineClone.originalEngine; + this._newIndex = aNewIndex; +} +EngineMoveOp.prototype = { + _engine: null, + _newIndex: null, + commit: function() { + Services.search.moveEngine(this._engine, this._newIndex); + } +} + +function EngineRemoveOp(aEngineClone) { + if (!aEngineClone) + throw new Error("bad args to new EngineRemoveOp!"); + this._engine = aEngineClone.originalEngine; +} +EngineRemoveOp.prototype = { + _engine: null, + commit: function() { + Services.search.removeEngine(this._engine); + } +} + +function EngineUnhideOp(aEngineClone, aNewIndex) { + if (!aEngineClone) + throw new Error("bad args to new EngineUnhideOp!"); + this._engine = aEngineClone.originalEngine; + this._newIndex = aNewIndex; +} +EngineUnhideOp.prototype = { + _engine: null, + _newIndex: null, + commit: function() { + this._engine.hidden = false; + Services.search.moveEngine(this._engine, this._newIndex); + } +} + +function EngineChangeOp(aEngineClone, aProp, aValue) { + if (!aEngineClone) + throw new Error("bad args to new EngineChangeOp!"); + + this._engine = aEngineClone.originalEngine; + this._prop = aProp; + this._newValue = aValue; +} +EngineChangeOp.prototype = { + _engine: null, + _prop: null, + _newValue: null, + commit: function() { + this._engine[this._prop] = this._newValue; + } +} + +function EngineStore() { + this._engines = Services.search.getVisibleEngines().map(this._cloneEngine); + this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine); + + this._ops = []; + + // check if we need to disable the restore defaults button + var someHidden = this._defaultEngines.some(function(e) e.hidden); + gEngineManagerDialog.showRestoreDefaults(someHidden); +} +EngineStore.prototype = { + _engines: null, + _defaultEngines: null, + _ops: null, + + get engines() { + return this._engines; + }, + set engines(val) { + this._engines = val; + return val; + }, + + _getIndexForEngine: function(aEngine) { + return this._engines.indexOf(aEngine); + }, + + _getEngineByName: function(aName) { + for each (var engine in this._engines) + if (engine.name == aName) + return engine; + + return null; + }, + + _cloneEngine: function(aEngine) { + var clonedObj={}; + for (var i in aEngine) + clonedObj[i] = aEngine[i]; + clonedObj.originalEngine = aEngine; + return clonedObj; + }, + + // Callback for Array's some(). A thisObj must be passed to some() + _isSameEngine: function(aEngineClone) { + return aEngineClone.originalEngine == this.originalEngine; + }, + + commit: function() { + var currentEngine = this._cloneEngine(Services.search.currentEngine); + for (var i = 0; i < this._ops.length; i++) + this._ops[i].commit(); + + // Restore currentEngine if it is a default engine that is still visible. + // Needed if the user deletes currentEngine and then restores it. + if (this._defaultEngines.some(this._isSameEngine, currentEngine) && + !currentEngine.originalEngine.hidden) + Services.search.currentEngine = currentEngine.originalEngine; + }, + + addEngine: function(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + moveEngine: function(aEngine, aNewIndex) { + if (aNewIndex < 0 || aNewIndex > this._engines.length - 1) + throw new Error("ES_moveEngine: invalid aNewIndex!"); + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("ES_moveEngine: invalid engine?"); + + if (index == aNewIndex) + return; // nothing to do + + // Move the engine in our internal store + var removedEngine = this._engines.splice(index, 1)[0]; + this._engines.splice(aNewIndex, 0, removedEngine); + + this._ops.push(new EngineMoveOp(aEngine, aNewIndex)); + }, + + removeEngine: function(aEngine) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines.splice(index, 1); + this._ops.push(new EngineRemoveOp(aEngine)); + if (this._defaultEngines.some(this._isSameEngine, aEngine)) + gEngineManagerDialog.showRestoreDefaults(true); + }, + + restoreDefaultEngines: function() { + var added = 0; + + for (var i = 0; i < this._defaultEngines.length; ++i) { + var e = this._defaultEngines[i]; + + // If the engine is already in the list, just move it. + if (this._engines.some(this._isSameEngine, e)) { + this.moveEngine(this._getEngineByName(e.name), i); + } else { + // Otherwise, add it back to our internal store + this._engines.splice(i, 0, e); + this._ops.push(new EngineUnhideOp(e, i)); + added++; + } + } + gEngineManagerDialog.showRestoreDefaults(false); + return added; + }, + + changeEngine: function(aEngine, aProp, aNewValue) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines[index][aProp] = aNewValue; + this._ops.push(new EngineChangeOp(aEngine, aProp, aNewValue)); + }, + + reloadIcons: function() { + this._engines.forEach(function(e) { + e.uri = e.originalEngine.uri; + }); + } +} + +function EngineView(aEngineStore) { + this._engineStore = aEngineStore; +} +EngineView.prototype = { + _engineStore: null, + tree: null, + + get lastIndex() { + return this.rowCount - 1; + }, + get selectedIndex() { + var seln = this.selection; + if (seln.getRangeCount() > 0) { + var min = {}; + seln.getRangeAt(0, min, {}); + return min.value; + } + return -1; + }, + get selectedEngine() { + return this._engineStore.engines[this.selectedIndex]; + }, + + // Helpers + rowCountChanged: function(index, count) { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function() { + this.tree.invalidate(); + }, + + ensureRowIsVisible: function(index) { + this.tree.ensureRowIsVisible(index); + }, + + getSourceIndexFromDrag: function(dataTransfer) { + return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); + }, + + // nsITreeView + get rowCount() { + return this._engineStore.engines.length; + }, + + getImageSrc: function(index, column) { + if (column.id == "engineName" && this._engineStore.engines[index].iconURI) + return this._engineStore.engines[index].iconURI.spec; + return ""; + }, + + getCellText: function(index, column) { + if (column.id == "engineName") + return this._engineStore.engines[index].name; + else if (column.id == "engineKeyword") + return this._engineStore.engines[index].alias; + return ""; + }, + + setTree: function(tree) { + this.tree = tree; + }, + + canDrop: function(targetIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + return (sourceIndex != -1 && + sourceIndex != targetIndex && + sourceIndex != targetIndex + orientation); + }, + + drop: function(dropIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + var sourceEngine = this._engineStore.engines[sourceIndex]; + + if (dropIndex > sourceIndex) { + if (orientation == Ci.nsITreeView.DROP_BEFORE) + dropIndex--; + } else { + if (orientation == Ci.nsITreeView.DROP_AFTER) + dropIndex++; + } + + this._engineStore.moveEngine(sourceEngine, dropIndex); + gEngineManagerDialog.showRestoreDefaults(true); + + // Redraw, and adjust selection + this.invalidate(); + this.selection.select(dropIndex); + }, + + selection: null, + getRowProperties: function(index) { return ""; }, + getCellProperties: function(index, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isContainerEmpty: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function(index) { return false; }, + getParentIndex: function(index) { return -1; }, + hasNextSibling: function(parentIndex, index) { return false; }, + getLevel: function(index) { return 0; }, + getProgressMode: function(index, column) { }, + getCellValue: function(index, column) { }, + toggleOpenState: function(index) { }, + cycleHeader: function(column) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(index, column) { return false; }, + isSelectable: function(index, column) { return false; }, + setCellValue: function(index, column, value) { }, + setCellText: function(index, column, value) { }, + performAction: function(action) { }, + performActionOnRow: function(action, index) { }, + performActionOnCell: function(action, index, column) { } +}; diff --git a/browser/components/search/content/engineManager.xul b/browser/components/search/content/engineManager.xul new file mode 100644 index 000000000..1152ef8db --- /dev/null +++ b/browser/components/search/content/engineManager.xul @@ -0,0 +1,93 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/engineManager.css"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/engineManager.dtd"> + +<dialog id="engineManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + buttons="accept,cancel,extra2" + buttonlabelextra2="&restoreDefaults.label;" + buttonaccesskeyextra2="&restoreDefaults.accesskey;" + onload="gEngineManagerDialog.init();" + onunload="gEngineManagerDialog.destroy();" + ondialogaccept="gEngineManagerDialog.onOK();" + ondialogextra2="gEngineManagerDialog.onRestoreDefaults();" + title="&engineManager.title;" + style="&engineManager.style;" + persist="screenX screenY width height" + windowtype="Browser:SearchManager"> + + <script type="application/javascript" + src="chrome://browser/content/search/engineManager.js"/> + + <commandset id="engineManagerCommandSet"> + <command id="cmd_remove" + oncommand="gEngineManagerDialog.remove();" + disabled="true"/> + <command id="cmd_moveup" + oncommand="gEngineManagerDialog.bump(1);" + disabled="true"/> + <command id="cmd_movedown" + oncommand="gEngineManagerDialog.bump(-1);" + disabled="true"/> + <command id="cmd_editkeyword" + oncommand="gEngineManagerDialog.editKeyword().catch(Components.utils.reportError);" + disabled="true"/> + </commandset> + + <keyset id="engineManagerKeyset"> + <key id="delete" keycode="VK_DELETE" command="cmd_remove"/> + </keyset> + + <stringbundleset id="engineManagerBundleset"> + <stringbundle id="engineManagerBundle" src="chrome://browser/locale/engineManager.properties"/> + </stringbundleset> + + <description>&engineManager.intro;</description> + <separator class="thin"/> + <hbox flex="1"> + <tree id="engineList" flex="1" rows="10" hidecolumnpicker="true" + seltype="single" onselect="gEngineManagerDialog.onSelect();"> + <treechildren id="engineChildren" flex="1" + ondragstart="onDragEngineStart(event);"/> + <treecols> + <treecol id="engineName" flex="4" label="&columnLabel.name;"/> + <treecol id="engineKeyword" flex="1" label="&columnLabel.keyword;"/> + </treecols> + </tree> + <vbox> + <spacer flex="1"/> + <button id="edit" + label="&edit.label;" + accesskey="&edit.accesskey;" + command="cmd_editkeyword"/> + <button id="up" + label="&up.label;" + accesskey="&up.accesskey;" + command="cmd_moveup"/> + <button id="down" + label="&dn.label;" + accesskey="&dn.accesskey;" + command="cmd_movedown"/> + <spacer flex="1"/> + <button id="remove" + label="&remove.label;" + accesskey="&remove.accesskey;" + command="cmd_remove"/> + </vbox> + </hbox> + <hbox> + <checkbox id="enableSuggest" + label="&enableSuggest.label;" + accesskey="&enableSuggest.accesskey;"/> + </hbox> + <hbox> + <label id="addEngines" class="text-link" value="&addEngine.label;" + onclick="if (event.button == 0) { gEngineManagerDialog.loadAddEngines(); }"/> + </hbox> +</dialog> diff --git a/browser/components/search/content/search.xml b/browser/components/search/content/search.xml new file mode 100644 index 000000000..eccaa072a --- /dev/null +++ b/browser/components/search/content/search.xml @@ -0,0 +1,834 @@ +<?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"/> + + <xul:textbox class="searchbar-textbox" + anonid="searchbar-textbox" + type="autocomplete" + flex="1" + autocompletepopup="PopupAutoComplete" + autocompletesearch="search-autocomplete" + autocompletesearchparam="searchbar-history" + timeout="250" + maxrows="10" + completeselectedindex="true" + showcommentcolumn="true" + tabscrolling="true" + xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines"> + <xul:box> + <xul:button class="searchbar-engine-button" + type="menu" + anonid="searchbar-engine-button"> + <xul:image class="searchbar-engine-image" xbl:inherits="src"/> + <xul:image class="searchbar-dropmarker-image"/> + <xul:menupopup class="searchbar-popup" + anonid="searchbar-popup"> + <xul:menuseparator/> + <xul:menuitem class="open-engine-manager" + anonid="open-engine-manager" + label="&cmd_engineManager.label;" + oncommand="openManager(event);"/> + </xul:menupopup> + </xul:button> + </xul:box> + <xul:hbox class="search-go-container"> + <xul:image class="search-go-button" + 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; + + var os = + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "browser-search-engine-modified", false); + + this._initialized = true; + + this.searchService.init((function search_init_cb(aStatus) { + // Bail out if the binding has been destroyed + if (!this._initialized) + return; + + if (Components.isSuccessCode(aStatus)) { + // Refresh the display (updating icon, etc) + this.updateDisplay(); + } else { + Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus); + } + }).bind(this)); + ]]></constructor> + + <destructor><![CDATA[ + if (this._initialized) { + this._initialized = false; + + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.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. + if (this._textbox.mController.input == this) + this._textbox.mController.input = null; + ]]></destructor> + + <field name="_stringBundle">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-stringbundle");</field> + <field name="_textbox">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-textbox");</field> + <field name="_popup">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-popup");</field> + <field name="_ss">null</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 = this.searchService.getVisibleEngines(); + return this._engines; + ]]></getter> + </property> + + <field name="searchButton">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-engine-button");</field> + + <property name="currentEngine"> + <setter><![CDATA[ + let ss = this.searchService; + ss.defaultEngine = ss.currentEngine = val; + return val; + ]]></setter> + <getter><![CDATA[ + var currentEngine = this.searchService.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="searchService" readonly="true"> + <getter><![CDATA[ + if (!this._ss) { + const nsIBSS = Components.interfaces.nsIBrowserSearchService; + this._ss = + Components.classes["@mozilla.org/browser/search-service;1"] + .getService(nsIBSS); + } + return this._ss; + ]]></getter> + </property> + + <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-current": + // The current engine was changed. Rebuilding the menu appears to + // confuse its idea of whether it should be open when it's just + // been clicked, so we force it to close now. + this._popup.hidePopup(); + 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; + + // Rebuild the popup and update the display after any modification. + this.rebuildPopup(); + 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[ + var allbrowsers = getBrowser().mPanelContainer.childNodes; + for (var tab = 0; tab < allbrowsers.length; tab++) { + var browser = getBrowser().getBrowserAtIndex(tab); + 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; + } + } + } + } + ]]></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[ + var allbrowsers = getBrowser().mPanelContainer.childNodes; + for (var tab = 0; tab < allbrowsers.length; tab++) { + var browser = getBrowser().getBrowserAtIndex(tab); + 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; + } + } + } + } + ]]></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 = name; + this._textbox.label = text; + this._textbox.tooltipText = text; + ]]></body> + </method> + + <!-- Rebuilds the dynamic portion of the popup menu (i.e., the menu items + for new search engines that can be added to the available list). This + is called each time the popup is shown. + --> + <method name="rebuildPopupDynamic"> + <body><![CDATA[ + // We might not have added the main popup items yet, do that first + // if needed. + if (this._needToBuildPopup) + this.rebuildPopup(); + + var popup = this._popup; + // Clear any addengine menuitems, including addengine-item entries and + // the addengine-separator. Work backward to avoid invalidating the + // indexes as items are removed. + var items = popup.childNodes; + for (var i = items.length - 1; i >= 0; i--) { + if (items[i].classList.contains("addengine-item") || + items[i].classList.contains("addengine-separator")) + popup.removeChild(items[i]); + } + + var addengines = getBrowser().mCurrentBrowser.engines; + if (addengines && addengines.length > 0) { + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + // Find the (first) separator in the remaining menu, or the first item + // if no separators are present. + var insertLocation = popup.firstChild; + while (insertLocation.nextSibling && + insertLocation.localName != "menuseparator") { + insertLocation = insertLocation.nextSibling; + } + if (insertLocation.localName != "menuseparator") + insertLocation = popup.firstChild; + + var separator = document.createElementNS(kXULNS, "menuseparator"); + separator.setAttribute("class", "addengine-separator"); + popup.insertBefore(separator, insertLocation); + + // Insert the "add this engine" items. + for (var i = 0; i < addengines.length; i++) { + var menuitem = document.createElement("menuitem"); + var engineInfo = addengines[i]; + var labelStr = + this._stringBundle.getFormattedString("cmd_addFoundEngine", + [engineInfo.title]); + menuitem = document.createElementNS(kXULNS, "menuitem"); + menuitem.setAttribute("class", "menuitem-iconic addengine-item"); + menuitem.setAttribute("label", labelStr); + menuitem.setAttribute("tooltiptext", engineInfo.uri); + menuitem.setAttribute("uri", engineInfo.uri); + if (engineInfo.icon) + this.setIcon(menuitem, engineInfo.icon); + menuitem.setAttribute("title", engineInfo.title); + popup.insertBefore(menuitem, insertLocation); + } + } + ]]></body> + </method> + + <!-- Rebuilds the list of visible search engines in the menu. Does not remove + or update any dynamic entries (i.e., "Add this engine" items) nor the + Manage Engines item. This is called by the observer when the list of + visible engines, or the currently selected engine, has changed. + --> + <method name="rebuildPopup"> + <body><![CDATA[ + var popup = this._popup; + + // Clear the popup, down to the first separator + while (popup.firstChild && popup.firstChild.localName != "menuseparator") + popup.removeChild(popup.firstChild); + + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var engines = this.engines; + for (var i = engines.length - 1; i >= 0; --i) { + var menuitem = document.createElementNS(kXULNS, "menuitem"); + var name = engines[i].name; + menuitem.setAttribute("label", name); + menuitem.setAttribute("id", name); + menuitem.setAttribute("class", "menuitem-iconic searchbar-engine-menuitem menuitem-with-favicon"); + // Since this menu is rebuilt by the observer method whenever a new + // engine is selected, the "selected" attribute does not need to be + // explicitly cleared anywhere. + if (engines[i] == this.currentEngine) + menuitem.setAttribute("selected", "true"); + var tooltip = this._stringBundle.getFormattedString("searchtip", [name]); + menuitem.setAttribute("tooltiptext", tooltip); + if (engines[i].iconURI) + this.setIcon(menuitem, engines[i].iconURI.spec); + popup.insertBefore(menuitem, popup.firstChild); + menuitem.engine = engines[i]; + } + + this._needToBuildPopup = false; + ]]></body> + </method> + + <method name="openManager"> + <parameter name="aEvent"/> + <body><![CDATA[ + var wm = + Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + + var window = wm.getMostRecentWindow("Browser:SearchManager"); + if (window) + window.focus() + else { + setTimeout(function () { + openDialog("chrome://browser/content/search/engineManager.xul", + "_blank", "chrome,dialog,modal,centerscreen,resizable"); + }, 0); + } + ]]></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(); + ]]></body> + </method> + + <method name="handleSearchCommand"> + <parameter name="aEvent"/> + <body><![CDATA[ + var textBox = this._textbox; + var textValue = textBox.value; + + var where = "current"; + if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") { + if (aEvent.button == 2) + return; + where = whereToOpenLink(aEvent, false, true); + } + else { + var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab"); + if ((aEvent && aEvent.altKey) ^ newTabPref) + where = "tab"; + } + + this.doSearch(textValue, where); + ]]></body> + </method> + + <method name="doSearch"> + <parameter name="aData"/> + <parameter name="aWhere"/> + <body><![CDATA[ + var textBox = this._textbox; + + // Save the current value in the form history + if (aData && !PrivateBrowsingUtils.isWindowPrivate(window)) { + 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); + }}); + } + + // null parameter below specifies HTML response for search + var submission = this.currentEngine.getSubmission(aData); + openUILinkIn(submission.uri.spec, aWhere, null, submission.postData); + ]]></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")) { + var searchService = + Components.classes["@mozilla.org/browser/search-service;1"] + .getService(Components.interfaces.nsIBrowserSearchService); + // We only detect OpenSearch files + var type = Components.interfaces.nsISearchEngine.DATA_XML; + // Select the installed engine if the installation succeeds + var installCallback = { + onSuccess: engine => this.currentEngine = engine + } + searchService.addEngine(target.getAttribute("uri"), type, + target.getAttribute("src"), false, + installCallback); + } + else + return; + + this.focus(); + this.select(); + ]]></handler> + + <handler event="popupshowing" action="this.rebuildPopupDynamic();"/> + + <handler event="DOMMouseScroll" + phase="capturing" + modifiers="accel" + action="this.selectEngine(event, (event.detail > 0));"/> + + <handler event="focus"> + <![CDATA[ + // Speculatively connect to the current engine's search URI (and + // suggest URI, if different) to reduce request latency + + const SUGGEST_TYPE = "application/x-suggestions+json"; + var engine = this.currentEngine; + var connector = + Services.io.QueryInterface(Components.interfaces.nsISpeculativeConnect); + var searchURI = engine.getSubmission("dummy").uri; + let callbacks = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsILoadContext); + connector.speculativeConnect(searchURI, callbacks); + + if (engine.supportsResponseType(SUGGEST_TYPE)) { + var suggestURI = engine.getSubmission("dummy", SUGGEST_TYPE).uri; + if (suggestURI.prePath != searchURI.prePath) + connector.speculativeConnect(suggestURI, callbacks); + } + ]]></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._prefBranch = + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + this._suggestEnabled = + this._prefBranch.getBoolPref("browser.search.suggest.enabled"); + this._clickSelectsAll = + this._prefBranch.getBoolPref("browser.urlbar.clickSelectsAll"); + + this.setAttribute("clickSelectsAll", this._clickSelectsAll); + + // 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() { + 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); + + 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.searchBar.select(); goDoCommand('cmd_paste'); BrowserSearch.searchBar.handleSearchCommand();"); + 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.controllers.appendController(this.searchbarController); + + // Add observer for suggest preference + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + prefs.addObserver("browser.search.suggest.enabled", this, false); + prefs.addObserver("browser.urlbar.clickSelectsAll", this, false); + ]]></constructor> + + <destructor><![CDATA[ + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + prefs.removeObserver("browser.search.suggest.enabled", this); + prefs.removeObserver("browser.urlbar.clickSelectsAll", 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="_prefBranch"/> + <field name="_suggestMenuItem"/> + <field name="_suggestEnabled"/> + <field name="_clickSelectsAll"/> + + <!-- + 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 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 (PopupAutoComplete + // 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; + + popup.mInput = this; + popup.view = this.controller.QueryInterface(Components.interfaces.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(); + if (isRTL) { + var width = innerRect.right - outerRect.left; + } else { + var width = 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") { + switch (aData) { + case "browser.search.suggest.enabled": + this._suggestEnabled = this._prefBranch.getBoolPref(aData); + this._suggestMenuItem.setAttribute("checked", this._suggestEnabled); + break; + case "browser.urlbar.clickSelectsAll": + this._clickSelectsAll = this._prefBranch.getBoolPref(aData); + this.setAttribute("clickSelectsAll", this._clickSelectsAll); + break; + } + } + ]]></body> + </method> + + <method name="openSearch"> + <body> + <![CDATA[ + // Don't open search popup if history popup is open + if (!this.popupOpen) { + document.getBindingParent(this).searchButton.open = true; + return false; + } + return true; + ]]> + </body> + </method> + + <!-- override |onTextEntered| in autocomplete.xml --> + <method name="onTextEntered"> + <parameter name="aEvent"/> + <body><![CDATA[ + var evt = aEvent || this.mEnterEvent; + document.getBindingParent(this).handleSearchCommand(evt); + this.mEnterEvent = null; + ]]></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"); + + let searchBar = this._self.parentNode; + + 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. + this._self._prefBranch.setBoolPref("browser.search.suggest.enabled", + !this._self._suggestEnabled); + break; + default: + // do nothing with unrecognized command + } + } + })]]></field> + </implementation> + + <handlers> + <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="keypress" keycode="VK_F4" + phase="capturing" + action="return this.openSearch();"/> + + <handler event="dragover"> + <![CDATA[ + var types = event.dataTransfer.types; + if (types.contains("text/plain") || types.contains("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; + this.onTextEntered(event); + } + ]]> + </handler> + + </handlers> + </binding> +</bindings> diff --git a/browser/components/search/content/searchbarBindings.css b/browser/components/search/content/searchbarBindings.css new file mode 100644 index 000000000..b20e2157a --- /dev/null +++ b/browser/components/search/content/searchbarBindings.css @@ -0,0 +1,13 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +.searchbar-textbox { + -moz-binding: url("chrome://browser/content/search/search.xml#searchbar-textbox"); +} + +.searchbar-engine-button { + -moz-user-focus: none; +} diff --git a/browser/components/search/jar.mn b/browser/components/search/jar.mn new file mode 100644 index 000000000..71f6ba45e --- /dev/null +++ b/browser/components/search/jar.mn @@ -0,0 +1,9 @@ +# 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/. + +browser.jar: + content/browser/search/search.xml (content/search.xml) + content/browser/search/searchbarBindings.css (content/searchbarBindings.css) + content/browser/search/engineManager.xul (content/engineManager.xul) + content/browser/search/engineManager.js (content/engineManager.js) diff --git a/browser/components/search/moz.build b/browser/components/search/moz.build new file mode 100644 index 000000000..ecb79e730 --- /dev/null +++ b/browser/components/search/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file |