/* 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.offUpdate(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 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.id == this.originalEngine.id; }, addEngine(aEngine) { this._engines.push(this._cloneEngine(aEngine)); }, updateEngine(newEngine) { let engineToUpdate = this._engines.findIndex( e => e.originalEngine.id == newEngine.id ); 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); } }); } }, };