summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/search.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences/search.js')
-rw-r--r--browser/components/preferences/search.js1098
1 files changed, 1098 insertions, 0 deletions
diff --git a/browser/components/preferences/search.js b/browser/components/preferences/search.js
new file mode 100644
index 0000000000..38d084ba10
--- /dev/null
+++ b/browser/components/preferences/search.js
@@ -0,0 +1,1098 @@
+/* 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/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
+});
+
+Preferences.addAll([
+ { id: "browser.search.suggest.enabled", type: "bool" },
+ { id: "browser.urlbar.suggest.searches", type: "bool" },
+ { id: "browser.search.suggest.enabled.private", type: "bool" },
+ { id: "browser.search.hiddenOneOffs", type: "unichar" },
+ { id: "browser.search.widget.inNavBar", type: "bool" },
+ { id: "browser.urlbar.showSearchSuggestionsFirst", type: "bool" },
+ { id: "browser.urlbar.showSearchTerms.enabled", type: "bool" },
+ { id: "browser.search.separatePrivateDefault", type: "bool" },
+ { id: "browser.search.separatePrivateDefault.ui.enabled", type: "bool" },
+]);
+
+const ENGINE_FLAVOR = "text/x-moz-search-engine";
+const SEARCH_TYPE = "default_search";
+const SEARCH_KEY = "defaultSearch";
+
+var gEngineView = null;
+
+var gSearchPane = {
+ init() {
+ gEngineView = new EngineView(new EngineStore());
+ document.getElementById("engineList").view = gEngineView;
+ this.buildDefaultEngineDropDowns().catch(console.error);
+
+ if (
+ Services.policies &&
+ !Services.policies.isAllowed("installSearchEngine")
+ ) {
+ document.getElementById("addEnginesBox").hidden = true;
+ } else {
+ let addEnginesLink = document.getElementById("addEngines");
+ addEnginesLink.setAttribute("href", lazy.SearchUIUtils.searchEnginesURL);
+ }
+
+ window.addEventListener("click", this);
+ window.addEventListener("command", this);
+ window.addEventListener("dragstart", this);
+ window.addEventListener("keypress", this);
+ window.addEventListener("select", this);
+ window.addEventListener("dblclick", this);
+
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ Services.obs.addObserver(this, "intl:app-locales-changed");
+ window.addEventListener("unload", () => {
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ Services.obs.removeObserver(this, "intl:app-locales-changed");
+ });
+
+ let suggestsPref = Preferences.get("browser.search.suggest.enabled");
+ let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
+ let privateSuggestsPref = Preferences.get(
+ "browser.search.suggest.enabled.private"
+ );
+ let updateSuggestionCheckboxes = this._updateSuggestionCheckboxes.bind(
+ this
+ );
+ suggestsPref.on("change", updateSuggestionCheckboxes);
+ urlbarSuggestsPref.on("change", updateSuggestionCheckboxes);
+ let urlbarSuggests = document.getElementById("urlBarSuggestion");
+ urlbarSuggests.addEventListener("command", () => {
+ urlbarSuggestsPref.value = urlbarSuggests.checked;
+ });
+ let privateWindowCheckbox = document.getElementById(
+ "showSearchSuggestionsPrivateWindows"
+ );
+ privateWindowCheckbox.addEventListener("command", () => {
+ privateSuggestsPref.value = privateWindowCheckbox.checked;
+ });
+
+ setEventListener(
+ "browserSeparateDefaultEngine",
+ "command",
+ this._onBrowserSeparateDefaultEngineChange.bind(this)
+ );
+ setEventListener("openLocationBarPrivacyPreferences", "click", function(
+ event
+ ) {
+ if (event.button == 0) {
+ gotoPref("privacy-locationBar");
+ }
+ });
+
+ this._initDefaultEngines();
+ this._initShowSearchTermsCheckbox();
+ this._updateSuggestionCheckboxes();
+ this._showAddEngineButton();
+ },
+
+ /**
+ * Initialize the default engine handling. This will hide the private default
+ * options if they are not enabled yet.
+ */
+ _initDefaultEngines() {
+ this._separatePrivateDefaultEnabledPref = Preferences.get(
+ "browser.search.separatePrivateDefault.ui.enabled"
+ );
+
+ this._separatePrivateDefaultPref = Preferences.get(
+ "browser.search.separatePrivateDefault"
+ );
+
+ const checkbox = document.getElementById("browserSeparateDefaultEngine");
+ checkbox.checked = !this._separatePrivateDefaultPref.value;
+
+ this._updatePrivateEngineDisplayBoxes();
+
+ const listener = () => {
+ this._updatePrivateEngineDisplayBoxes();
+ this.buildDefaultEngineDropDowns().catch(console.error);
+ };
+
+ this._separatePrivateDefaultEnabledPref.on("change", listener);
+ this._separatePrivateDefaultPref.on("change", listener);
+ },
+
+ _initShowSearchTermsCheckbox() {
+ let checkbox = document.getElementById("searchShowSearchTermCheckbox");
+
+ // Add Nimbus event to show/hide checkbox.
+ let onNimbus = () => {
+ checkbox.hidden = !UrlbarPrefs.get("showSearchTermsFeatureGate");
+ };
+ NimbusFeatures.urlbar.onUpdate(onNimbus);
+
+ // Add observer of Search Bar preference as showSearchTerms
+ // can't be enabled/disabled while Search Bar is enabled.
+ let searchBarPref = Preferences.get("browser.search.widget.inNavBar");
+ let updateCheckboxEnabled = () => {
+ checkbox.disabled = searchBarPref.value;
+ };
+ searchBarPref.on("change", updateCheckboxEnabled);
+
+ // Fire once to initialize.
+ onNimbus();
+ updateCheckboxEnabled();
+
+ window.addEventListener("unload", () => {
+ NimbusFeatures.urlbar.off(onNimbus);
+ });
+ },
+
+ _updatePrivateEngineDisplayBoxes() {
+ const separateEnabled = this._separatePrivateDefaultEnabledPref.value;
+ document.getElementById(
+ "browserSeparateDefaultEngine"
+ ).hidden = !separateEnabled;
+
+ const separateDefault = this._separatePrivateDefaultPref.value;
+
+ const vbox = document.getElementById("browserPrivateEngineSelection");
+ vbox.hidden = !separateEnabled || !separateDefault;
+ },
+
+ _onBrowserSeparateDefaultEngineChange(event) {
+ this._separatePrivateDefaultPref.value = !event.target.checked;
+ },
+
+ _updateSuggestionCheckboxes() {
+ let suggestsPref = Preferences.get("browser.search.suggest.enabled");
+ let permanentPB = Services.prefs.getBoolPref(
+ "browser.privatebrowsing.autostart"
+ );
+ let urlbarSuggests = document.getElementById("urlBarSuggestion");
+ let positionCheckbox = document.getElementById(
+ "showSearchSuggestionsFirstCheckbox"
+ );
+ let privateWindowCheckbox = document.getElementById(
+ "showSearchSuggestionsPrivateWindows"
+ );
+
+ urlbarSuggests.disabled = !suggestsPref.value || permanentPB;
+ privateWindowCheckbox.disabled = !suggestsPref.value;
+ privateWindowCheckbox.checked = Preferences.get(
+ "browser.search.suggest.enabled.private"
+ ).value;
+ if (privateWindowCheckbox.disabled) {
+ privateWindowCheckbox.checked = false;
+ }
+
+ let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
+ urlbarSuggests.checked = urlbarSuggestsPref.value;
+ if (urlbarSuggests.disabled) {
+ urlbarSuggests.checked = false;
+ }
+
+ if (urlbarSuggests.checked) {
+ positionCheckbox.disabled = false;
+ // Update the checked state of the show-suggestions-first checkbox. Note
+ // that this does *not* also update its pref, it only checks the box.
+ positionCheckbox.checked = Preferences.get(
+ positionCheckbox.getAttribute("preference")
+ ).value;
+ } else {
+ positionCheckbox.disabled = true;
+ positionCheckbox.checked = false;
+ }
+
+ let permanentPBLabel = document.getElementById(
+ "urlBarSuggestionPermanentPBLabel"
+ );
+ permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB;
+ },
+
+ _showAddEngineButton() {
+ let aliasRefresh = Services.prefs.getBoolPref(
+ "browser.urlbar.update2.engineAliasRefresh",
+ false
+ );
+ if (aliasRefresh) {
+ let addButton = document.getElementById("addEngineButton");
+ addButton.hidden = false;
+ }
+ },
+
+ /**
+ * Builds the default and private engines drop down lists. This is called
+ * each time something affects the list of engines.
+ */
+ async buildDefaultEngineDropDowns() {
+ await this._buildEngineDropDown(
+ document.getElementById("defaultEngine"),
+ (await Services.search.getDefault()).name,
+ false
+ );
+
+ if (this._separatePrivateDefaultEnabledPref.value) {
+ await this._buildEngineDropDown(
+ document.getElementById("defaultPrivateEngine"),
+ (await Services.search.getDefaultPrivate()).name,
+ true
+ );
+ }
+ },
+
+ /**
+ * Builds a drop down menu of search engines.
+ *
+ * @param {DOMMenuList} list
+ * The menu list element to attach the list of engines.
+ * @param {string} currentEngine
+ * The name of the current default engine.
+ * @param {boolean} isPrivate
+ * True if we are dealing with the default engine for private mode.
+ */
+ async _buildEngineDropDown(list, currentEngine, isPrivate) {
+ // If the current engine isn't in the list any more, select the first item.
+ let engines = gEngineView._engineStore._engines;
+ if (!engines.length) {
+ return;
+ }
+ if (!engines.some(e => e.name == currentEngine)) {
+ currentEngine = engines[0].name;
+ }
+
+ // Now clean-up and rebuild the list.
+ list.removeAllItems();
+ gEngineView._engineStore._engines.forEach(e => {
+ let item = list.appendItem(e.name);
+ item.setAttribute(
+ "class",
+ "menuitem-iconic searchengine-menuitem menuitem-with-favicon"
+ );
+ if (e.iconURI) {
+ item.setAttribute("image", e.iconURI.spec);
+ }
+ item.engine = e;
+ if (e.name == currentEngine) {
+ list.selectedItem = item;
+ }
+ });
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "dblclick":
+ if (aEvent.target.id == "engineChildren") {
+ let cell = aEvent.target.parentNode.getCellAt(
+ aEvent.clientX,
+ aEvent.clientY
+ );
+ if (cell.col?.id == "engineKeyword") {
+ this.startEditingAlias(gEngineView.selectedIndex);
+ }
+ }
+ break;
+ case "click":
+ if (
+ aEvent.target.id != "engineChildren" &&
+ !aEvent.target.classList.contains("searchEngineAction")
+ ) {
+ let engineList = document.getElementById("engineList");
+ // We don't want to toggle off selection while editing keyword
+ // so proceed only when the input field is hidden.
+ // We need to check that engineList.view is defined here
+ // because the "click" event listener is on <window> and the
+ // view might have been destroyed if the pane has been navigated
+ // away from.
+ if (engineList.inputField.hidden && engineList.view) {
+ let selection = engineList.view.selection;
+ if (selection.count > 0) {
+ selection.toggleSelect(selection.currentIndex);
+ }
+ engineList.blur();
+ }
+ }
+ break;
+ case "command":
+ switch (aEvent.target.id) {
+ case "":
+ if (
+ aEvent.target.parentNode &&
+ aEvent.target.parentNode.parentNode
+ ) {
+ if (aEvent.target.parentNode.parentNode.id == "defaultEngine") {
+ gSearchPane.setDefaultEngine();
+ } else if (
+ aEvent.target.parentNode.parentNode.id == "defaultPrivateEngine"
+ ) {
+ gSearchPane.setDefaultPrivateEngine();
+ }
+ }
+ break;
+ case "restoreDefaultSearchEngines":
+ gSearchPane.onRestoreDefaults();
+ break;
+ case "removeEngineButton":
+ Services.search.removeEngine(
+ gEngineView.selectedEngine.originalEngine
+ );
+ break;
+ case "addEngineButton":
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/addEngine.xhtml",
+ { features: "resizable=no, modal=yes" }
+ );
+ break;
+ }
+ break;
+ case "dragstart":
+ if (aEvent.target.id == "engineChildren") {
+ onDragEngineStart(aEvent);
+ }
+ break;
+ case "keypress":
+ if (aEvent.target.id == "engineList") {
+ gSearchPane.onTreeKeyPress(aEvent);
+ }
+ break;
+ case "select":
+ if (aEvent.target.id == "engineList") {
+ gSearchPane.onTreeSelect();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Handle when the app locale is changed.
+ */
+ async appLocalesChanged() {
+ await document.l10n.ready;
+ await gEngineView.loadL10nNames();
+ },
+
+ /**
+ * Update the default engine UI and engine tree view as appropriate when engine changes
+ * or locale changes occur.
+ *
+ * @param {Object} engine
+ * @param {string} data
+ */
+ browserSearchEngineModified(engine, data) {
+ engine.QueryInterface(Ci.nsISearchEngine);
+ switch (data) {
+ case "engine-added":
+ gEngineView._engineStore.addEngine(engine);
+ gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1);
+ gSearchPane.buildDefaultEngineDropDowns();
+ break;
+ case "engine-changed":
+ gSearchPane.buildDefaultEngineDropDowns();
+ gEngineView._engineStore.updateEngine(engine);
+ gEngineView.invalidate();
+ break;
+ case "engine-removed":
+ gSearchPane.remove(engine);
+ break;
+ case "engine-default": {
+ // If the user is going through the drop down using up/down keys, the
+ // dropdown may still be open (eg. on Windows) when engine-default is
+ // fired, so rebuilding the list unconditionally would get in the way.
+ let selectedEngine = document.getElementById("defaultEngine")
+ .selectedItem.engine;
+ if (selectedEngine.name != engine.name) {
+ gSearchPane.buildDefaultEngineDropDowns();
+ }
+ break;
+ }
+ case "engine-default-private": {
+ if (
+ this._separatePrivateDefaultEnabledPref.value &&
+ this._separatePrivateDefaultPref.value
+ ) {
+ // If the user is going through the drop down using up/down keys, the
+ // dropdown may still be open (eg. on Windows) when engine-default is
+ // fired, so rebuilding the list unconditionally would get in the way.
+ const selectedEngine = document.getElementById("defaultPrivateEngine")
+ .selectedItem.engine;
+ if (selectedEngine.name != engine.name) {
+ gSearchPane.buildDefaultEngineDropDowns();
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * nsIObserver implementation.
+ */
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "intl:app-locales-changed": {
+ this.appLocalesChanged();
+ break;
+ }
+ case "browser-search-engine-modified": {
+ this.browserSearchEngineModified(subject, data);
+ break;
+ }
+ }
+ },
+
+ onTreeSelect() {
+ document.getElementById(
+ "removeEngineButton"
+ ).disabled = !gEngineView.isEngineSelectedAndRemovable();
+ },
+
+ onTreeKeyPress(aEvent) {
+ let index = gEngineView.selectedIndex;
+ let tree = document.getElementById("engineList");
+ if (tree.hasAttribute("editing")) {
+ return;
+ }
+
+ if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) {
+ // Space toggles the checkbox.
+ let newValue = !gEngineView.getCellValue(
+ index,
+ tree.columns.getNamedColumn("engineShown")
+ );
+ gEngineView.setCellValue(
+ index,
+ tree.columns.getFirstColumn(),
+ newValue.toString()
+ );
+ // Prevent page from scrolling on the space key.
+ aEvent.preventDefault();
+ } else {
+ let isMac = Services.appinfo.OS == "Darwin";
+ if (
+ (isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) ||
+ (!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2)
+ ) {
+ this.startEditingAlias(index);
+ } else if (
+ aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (isMac &&
+ aEvent.shiftKey &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE &&
+ gEngineView.isEngineSelectedAndRemovable())
+ ) {
+ // Delete and Shift+Backspace (Mac) removes selected engine.
+ Services.search.removeEngine(gEngineView.selectedEngine.originalEngine);
+ }
+ }
+ },
+
+ startEditingAlias(index) {
+ // Local shortcut aliases can't be edited.
+ if (gEngineView._getLocalShortcut(index)) {
+ return;
+ }
+
+ let tree = document.getElementById("engineList");
+ let engine = gEngineView._engineStore.engines[index];
+ tree.startEditing(index, tree.columns.getLastColumn());
+ tree.inputField.value = engine.alias || "";
+ tree.inputField.select();
+ },
+
+ async onRestoreDefaults() {
+ let num = await gEngineView._engineStore.restoreDefaultEngines();
+ gEngineView.rowCountChanged(0, num);
+ gEngineView.invalidate();
+ },
+
+ showRestoreDefaults(aEnable) {
+ document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable;
+ },
+
+ remove(aEngine) {
+ let index = gEngineView._engineStore.removeEngine(aEngine);
+ if (!gEngineView.tree) {
+ // Only update the selection if it's visible in the UI.
+ return;
+ }
+
+ gEngineView.rowCountChanged(index, -1);
+ gEngineView.invalidate();
+
+ gEngineView.selection.select(Math.min(index, gEngineView.rowCount - 1));
+ gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
+
+ document.getElementById("engineList").focus();
+ },
+
+ async editKeyword(aEngine, aNewKeyword) {
+ let keyword = aNewKeyword.trim();
+ if (keyword) {
+ let eduplicate = false;
+ let dupName = "";
+
+ // Check for duplicates in Places keywords.
+ let bduplicate = !!(await PlacesUtils.keywords.fetch(keyword));
+
+ // Check for duplicates in changes we haven't committed yet
+ let engines = gEngineView._engineStore.engines;
+ let lc_keyword = keyword.toLocaleLowerCase();
+ for (let engine of engines) {
+ if (
+ engine.alias &&
+ engine.alias.toLocaleLowerCase() == lc_keyword &&
+ engine.name != aEngine.name
+ ) {
+ eduplicate = true;
+ dupName = engine.name;
+ break;
+ }
+ }
+
+ // Notify the user if they have chosen an existing engine/bookmark keyword
+ if (eduplicate || bduplicate) {
+ let msgids = [{ id: "search-keyword-warning-title" }];
+ if (eduplicate) {
+ msgids.push({
+ id: "search-keyword-warning-engine",
+ args: { name: dupName },
+ });
+ } else {
+ msgids.push({ id: "search-keyword-warning-bookmark" });
+ }
+
+ let [dtitle, msg] = await document.l10n.formatValues(msgids);
+
+ Services.prompt.alert(window, dtitle, msg);
+ return false;
+ }
+ }
+
+ gEngineView._engineStore.changeEngine(aEngine, "alias", keyword);
+ gEngineView.invalidate();
+ return true;
+ },
+
+ saveOneClickEnginesList() {
+ let hiddenList = [];
+ for (let engine of gEngineView._engineStore.engines) {
+ if (!engine.shown) {
+ hiddenList.push(engine.name);
+ }
+ }
+ Preferences.get("browser.search.hiddenOneOffs").value = hiddenList.join(
+ ","
+ );
+ },
+
+ async setDefaultEngine() {
+ await Services.search.setDefault(
+ document.getElementById("defaultEngine").selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ if (ExtensionSettingsStore.getSetting(SEARCH_TYPE, SEARCH_KEY) !== null) {
+ ExtensionSettingsStore.select(
+ ExtensionSettingsStore.SETTING_USER_SET,
+ SEARCH_TYPE,
+ SEARCH_KEY
+ );
+ }
+ },
+
+ async setDefaultPrivateEngine() {
+ await Services.search.setDefaultPrivate(
+ document.getElementById("defaultPrivateEngine").selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ },
+};
+
+function onDragEngineStart(event) {
+ var selectedIndex = gEngineView.selectedIndex;
+
+ // Local shortcut rows can't be dragged or re-ordered.
+ if (gEngineView._getLocalShortcut(selectedIndex)) {
+ event.preventDefault();
+ return;
+ }
+
+ var tree = document.getElementById("engineList");
+ let cell = tree.getCellAt(event.clientX, event.clientY);
+ if (selectedIndex >= 0 && !gEngineView.isCheckBox(cell.row, cell.col)) {
+ event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString());
+ event.dataTransfer.effectAllowed = "move";
+ }
+}
+
+function EngineStore() {
+ let pref = Preferences.get("browser.search.hiddenOneOffs").value;
+ this.hiddenList = pref ? pref.split(",") : [];
+
+ this._engines = [];
+ this._defaultEngines = [];
+ Promise.all([
+ Services.search.getVisibleEngines(),
+ Services.search.getAppProvidedEngines(),
+ ]).then(([visibleEngines, defaultEngines]) => {
+ for (let engine of visibleEngines) {
+ this.addEngine(engine);
+ gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1);
+ }
+ this._defaultEngines = defaultEngines.map(this._cloneEngine, this);
+ gSearchPane.buildDefaultEngineDropDowns();
+
+ // check if we need to disable the restore defaults button
+ var someHidden = this._defaultEngines.some(e => e.hidden);
+ gSearchPane.showRestoreDefaults(someHidden);
+ });
+}
+EngineStore.prototype = {
+ _engines: null,
+ _defaultEngines: null,
+
+ get engines() {
+ return this._engines;
+ },
+ set engines(val) {
+ this._engines = val;
+ },
+
+ _getIndexForEngine(aEngine) {
+ return this._engines.indexOf(aEngine);
+ },
+
+ _getEngineByName(aName) {
+ return this._engines.find(engine => engine.name == aName);
+ },
+
+ _cloneEngine(aEngine) {
+ var clonedObj = {};
+ for (let i of ["id", "name", "alias", "iconURI", "hidden"]) {
+ clonedObj[i] = aEngine[i];
+ }
+ clonedObj.originalEngine = aEngine;
+ clonedObj.shown = !this.hiddenList.includes(clonedObj.name);
+ return clonedObj;
+ },
+
+ // Callback for Array's some(). A thisObj must be passed to some()
+ _isSameEngine(aEngineClone) {
+ return aEngineClone.originalEngine == this.originalEngine;
+ },
+
+ addEngine(aEngine) {
+ this._engines.push(this._cloneEngine(aEngine));
+ },
+
+ updateEngine(newEngine) {
+ let engineToUpdate = this._engines.findIndex(
+ e => e.originalEngine == newEngine
+ );
+ if (engineToUpdate != -1) {
+ this.engines[engineToUpdate] = this._cloneEngine(newEngine);
+ }
+ },
+
+ moveEngine(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 Promise.resolve();
+ } // nothing to do
+
+ // Move the engine in our internal store
+ var removedEngine = this._engines.splice(index, 1)[0];
+ this._engines.splice(aNewIndex, 0, removedEngine);
+
+ return Services.search.moveEngine(aEngine.originalEngine, aNewIndex);
+ },
+
+ removeEngine(aEngine) {
+ if (this._engines.length == 1) {
+ throw new Error("Cannot remove last engine!");
+ }
+
+ let engineName = aEngine.name;
+ let index = this._engines.findIndex(element => element.name == engineName);
+
+ if (index == -1) {
+ throw new Error("invalid engine?");
+ }
+
+ this._engines.splice(index, 1)[0];
+
+ if (aEngine.isAppProvided) {
+ gSearchPane.showRestoreDefaults(true);
+ }
+ gSearchPane.buildDefaultEngineDropDowns();
+ return index;
+ },
+
+ async restoreDefaultEngines() {
+ 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)) {
+ await this.moveEngine(this._getEngineByName(e.name), i);
+ } else {
+ // Otherwise, add it back to our internal store
+
+ // The search service removes the alias when an engine is hidden,
+ // so clear any alias we may have cached before unhiding the engine.
+ e.alias = "";
+
+ this._engines.splice(i, 0, e);
+ let engine = e.originalEngine;
+ engine.hidden = false;
+ await Services.search.moveEngine(engine, i);
+ added++;
+ }
+ }
+
+ // We can't do this as part of the loop above because the indices are
+ // used for moving engines.
+ let policyRemovedEngineNames =
+ Services.policies.getActivePolicies()?.SearchEngines?.Remove || [];
+ for (let engineName of policyRemovedEngineNames) {
+ let engine = Services.search.getEngineByName(engineName);
+ if (engine) {
+ try {
+ await Services.search.removeEngine(engine);
+ } catch (ex) {
+ // Engine might not exist
+ }
+ }
+ }
+
+ Services.search.resetToAppDefaultEngine();
+ gSearchPane.showRestoreDefaults(false);
+ gSearchPane.buildDefaultEngineDropDowns();
+ return added;
+ },
+
+ changeEngine(aEngine, aProp, aNewValue) {
+ var index = this._getIndexForEngine(aEngine);
+ if (index == -1) {
+ throw new Error("invalid engine?");
+ }
+
+ this._engines[index][aProp] = aNewValue;
+ aEngine.originalEngine[aProp] = aNewValue;
+ },
+
+ reloadIcons() {
+ this._engines.forEach(function(e) {
+ e.iconURI = e.originalEngine.iconURI;
+ });
+ },
+};
+
+function EngineView(aEngineStore) {
+ this._engineStore = aEngineStore;
+
+ UrlbarPrefs.addObserver(this);
+
+ this.loadL10nNames();
+}
+
+EngineView.prototype = {
+ _engineStore: null,
+ tree: null,
+
+ loadL10nNames() {
+ // This maps local shortcut sources to their l10n names. The names are needed
+ // by getCellText. Getting the names is async but getCellText is not, so we
+ // cache them here to retrieve them syncronously in getCellText.
+ this._localShortcutL10nNames = new Map();
+ return document.l10n
+ .formatValues(
+ UrlbarUtils.LOCAL_SEARCH_MODES.map(mode => {
+ let name = UrlbarUtils.getResultSourceName(mode.source);
+ return { id: `urlbar-search-mode-${name}` };
+ })
+ )
+ .then(names => {
+ for (let { source } of UrlbarUtils.LOCAL_SEARCH_MODES) {
+ this._localShortcutL10nNames.set(source, names.shift());
+ }
+ // Invalidate the tree now that we have the names in case getCellText was
+ // called before name retrieval finished.
+ this.invalidate();
+ });
+ },
+
+ get lastEngineIndex() {
+ return this._engineStore.engines.length - 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(index, count) {
+ if (this.tree) {
+ this.tree.rowCountChanged(index, count);
+ }
+ },
+
+ invalidate() {
+ this.tree?.invalidate();
+ },
+
+ ensureRowIsVisible(index) {
+ this.tree.ensureRowIsVisible(index);
+ },
+
+ getSourceIndexFromDrag(dataTransfer) {
+ return parseInt(dataTransfer.getData(ENGINE_FLAVOR));
+ },
+
+ isCheckBox(index, column) {
+ return column.id == "engineShown";
+ },
+
+ isEngineSelectedAndRemovable() {
+ let defaultEngine = Services.search.defaultEngine;
+ let defaultPrivateEngine = Services.search.defaultPrivateEngine;
+ // We don't allow the last remaining engine to be removed, thus the
+ // `this.lastEngineIndex != 0` check.
+ // We don't allow the default engine to be removed.
+ return (
+ this.selectedIndex != -1 &&
+ this.lastEngineIndex != 0 &&
+ !this._getLocalShortcut(this.selectedIndex) &&
+ this.selectedEngine.name != defaultEngine.name &&
+ this.selectedEngine.name != defaultPrivateEngine.name
+ );
+ },
+
+ /**
+ * Returns the local shortcut corresponding to a tree row, or null if the row
+ * is not a local shortcut.
+ *
+ * @param {number} index
+ * The tree row index.
+ * @returns {object}
+ * The local shortcut object or null if the row is not a local shortcut.
+ */
+ _getLocalShortcut(index) {
+ let engineCount = this._engineStore.engines.length;
+ if (index < engineCount) {
+ return null;
+ }
+ return UrlbarUtils.LOCAL_SEARCH_MODES[index - engineCount];
+ },
+
+ /**
+ * Called by UrlbarPrefs when a urlbar pref changes.
+ *
+ * @param {string} pref
+ * The name of the pref relative to the browser.urlbar branch.
+ */
+ onPrefChanged(pref) {
+ // If one of the local shortcut prefs was toggled, toggle its row's
+ // checkbox.
+ let parts = pref.split(".");
+ if (parts[0] == "shortcuts" && parts[1] && parts.length == 2) {
+ this.invalidate();
+ }
+ },
+
+ // nsITreeView
+ get rowCount() {
+ return (
+ this._engineStore.engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length
+ );
+ },
+
+ getImageSrc(index, column) {
+ if (column.id == "engineName") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return shortcut.icon;
+ }
+
+ if (this._engineStore.engines[index].iconURI) {
+ return this._engineStore.engines[index].iconURI.spec;
+ }
+
+ if (window.devicePixelRatio > 1) {
+ return "chrome://browser/skin/search-engine-placeholder@2x.png";
+ }
+ return "chrome://browser/skin/search-engine-placeholder.png";
+ }
+
+ return "";
+ },
+
+ getCellText(index, column) {
+ if (column.id == "engineName") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return this._localShortcutL10nNames.get(shortcut.source) || "";
+ }
+ return this._engineStore.engines[index].name;
+ } else if (column.id == "engineKeyword") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return shortcut.restrict;
+ }
+ return this._engineStore.engines[index].originalEngine.aliases.join(", ");
+ }
+ return "";
+ },
+
+ setTree(tree) {
+ this.tree = tree;
+ },
+
+ canDrop(targetIndex, orientation, dataTransfer) {
+ var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
+ return (
+ sourceIndex != -1 &&
+ sourceIndex != targetIndex &&
+ sourceIndex != targetIndex + orientation &&
+ // Local shortcut rows can't be dragged or dropped on.
+ targetIndex < this._engineStore.engines.length
+ );
+ },
+
+ async drop(dropIndex, orientation, dataTransfer) {
+ // Local shortcut rows can't be dragged or dropped on. This can sometimes
+ // be reached even though canDrop returns false for these rows.
+ if (this._engineStore.engines.length <= dropIndex) {
+ return;
+ }
+
+ var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
+ var sourceEngine = this._engineStore.engines[sourceIndex];
+
+ const nsITreeView = Ci.nsITreeView;
+ if (dropIndex > sourceIndex) {
+ if (orientation == nsITreeView.DROP_BEFORE) {
+ dropIndex--;
+ }
+ } else if (orientation == nsITreeView.DROP_AFTER) {
+ dropIndex++;
+ }
+
+ await this._engineStore.moveEngine(sourceEngine, dropIndex);
+ gSearchPane.showRestoreDefaults(true);
+ gSearchPane.buildDefaultEngineDropDowns();
+
+ // Redraw, and adjust selection
+ this.invalidate();
+ this.selection.select(dropIndex);
+ },
+
+ selection: null,
+ getRowProperties(index) {
+ return "";
+ },
+ getCellProperties(index, column) {
+ if (column.id == "engineName") {
+ // For local shortcut rows, return the result source name so we can style
+ // the icons in CSS.
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return UrlbarUtils.getResultSourceName(shortcut.source);
+ }
+ }
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ isContainer(index) {
+ return false;
+ },
+ isContainerOpen(index) {
+ return false;
+ },
+ isContainerEmpty(index) {
+ return false;
+ },
+ isSeparator(index) {
+ return false;
+ },
+ isSorted(index) {
+ return false;
+ },
+ getParentIndex(index) {
+ return -1;
+ },
+ hasNextSibling(parentIndex, index) {
+ return false;
+ },
+ getLevel(index) {
+ return 0;
+ },
+ getCellValue(index, column) {
+ if (column.id == "engineShown") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ return UrlbarPrefs.get(shortcut.pref);
+ }
+ return this._engineStore.engines[index].shown;
+ }
+ return undefined;
+ },
+ toggleOpenState(index) {},
+ cycleHeader(column) {},
+ selectionChanged() {},
+ cycleCell(row, column) {},
+ isEditable(index, column) {
+ return (
+ column.id != "engineName" &&
+ (column.id == "engineShown" || !this._getLocalShortcut(index))
+ );
+ },
+ setCellValue(index, column, value) {
+ if (column.id == "engineShown") {
+ let shortcut = this._getLocalShortcut(index);
+ if (shortcut) {
+ UrlbarPrefs.set(shortcut.pref, value == "true");
+ this.invalidate();
+ return;
+ }
+ this._engineStore.engines[index].shown = value == "true";
+ gEngineView.invalidate();
+ gSearchPane.saveOneClickEnginesList();
+ }
+ },
+ setCellText(index, column, value) {
+ if (column.id == "engineKeyword") {
+ gSearchPane
+ .editKeyword(this._engineStore.engines[index], value)
+ .then(valid => {
+ if (!valid) {
+ gSearchPane.startEditingAlias(index);
+ }
+ });
+ }
+ },
+};