From 086c044dc34dfc0f74fbe41f4ecb402b2cd34884 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:13:33 +0200 Subject: Merging upstream version 125.0.1. Signed-off-by: Daniel Baumann --- browser/components/preferences/search.js | 1121 +++++++++++++++++------------- 1 file changed, 626 insertions(+), 495 deletions(-) (limited to 'browser/components/preferences/search.js') diff --git a/browser/components/preferences/search.js b/browser/components/preferences/search.js index 42776cfa96..a491d6c5ca 100644 --- a/browser/components/preferences/search.js +++ b/browser/components/preferences/search.js @@ -9,6 +9,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", }); const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST = @@ -34,16 +35,27 @@ const ENGINE_FLAVOR = "text/x-moz-search-engine"; const SEARCH_TYPE = "default_search"; const SEARCH_KEY = "defaultSearch"; -// The name of in built engines that support trending results. -const TRENDING_ENGINES = ["Google", "Bing"]; - var gEngineView = null; var gSearchPane = { + _engineStore: null, + _engineDropDown: null, + _engineDropDownPrivate: null, + init() { - gEngineView = new EngineView(new EngineStore()); - document.getElementById("engineList").view = gEngineView; - this.buildDefaultEngineDropDowns().catch(console.error); + this._engineStore = new EngineStore(); + gEngineView = new EngineView(this._engineStore); + + this._engineDropDown = new DefaultEngineDropDown( + "normal", + this._engineStore + ); + this._engineDropDownPrivate = new DefaultEngineDropDown( + "private", + this._engineStore + ); + + this._engineStore.init().catch(console.error); if ( Services.policies && @@ -55,12 +67,7 @@ var gSearchPane = { 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"); @@ -115,7 +122,6 @@ var gSearchPane = { this._initDefaultEngines(); this._initShowSearchTermsCheckbox(); this._updateSuggestionCheckboxes(); - this._showAddEngineButton(); this._initRecentSeachesCheckbox(); this._initAddressBar(); }, @@ -140,7 +146,7 @@ var gSearchPane = { const listener = () => { this._updatePrivateEngineDisplayBoxes(); - this.buildDefaultEngineDropDowns().catch(console.error); + this._engineStore.notifyRebuildViews(); }; this._separatePrivateDefaultEnabledPref.on("change", listener); @@ -256,17 +262,6 @@ var gSearchPane = { this._updateTrendingCheckbox(!suggestsPref.value || permanentPB); }, - _showAddEngineButton() { - let aliasRefresh = Services.prefs.getBoolPref( - "browser.urlbar.update2.engineAliasRefresh", - false - ); - if (aliasRefresh) { - let addButton = document.getElementById("addEngineButton"); - addButton.hidden = false; - } - }, - _initRecentSeachesCheckbox() { this._recentSearchesEnabledPref = Preferences.get( "browser.urlbar.recentsearches.featureGate" @@ -285,38 +280,14 @@ var gSearchPane = { async _updateTrendingCheckbox(suggestDisabled) { let trendingBox = document.getElementById("showTrendingSuggestionsBox"); let trendingCheckBox = document.getElementById("showTrendingSuggestions"); - let trendingSupported = TRENDING_ENGINES.includes( - (await Services.search.getDefault()).name - ); + let trendingSupported = ( + await Services.search.getDefault() + ).supportsResponseType(lazy.SearchUtils.URL_TYPE.TRENDING_JSON); trendingBox.hidden = !Preferences.get("browser.urlbar.trending.featureGate") .value; trendingCheckBox.disabled = suggestDisabled || !trendingSupported; }, - /** - * 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 - ); - } - }, - // ADDRESS BAR /** @@ -486,125 +457,24 @@ var gSearchPane = { Services.prefs.clearUserPref(PREF_URLBAR_WEATHER_USER_ENABLED); }, - /** - * 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) { + handleEvent(aEvent) { + if (aEvent.type != "command") { 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.iconURL) { - item.setAttribute("image", e.iconURL); - } - 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(); + 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 "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; + default: + gEngineView.handleEvent(aEvent); } }, @@ -616,60 +486,6 @@ var gSearchPane = { 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(); - } - gSearchPane._updateSuggestionCheckboxes(); - 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. */ @@ -680,144 +496,25 @@ var gSearchPane = { 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); + let engine = subject.QueryInterface(Ci.nsISearchEngine); + switch (data) { + case "engine-default": { + // Pass through to the engine store to handle updates. + this._engineStore.browserSearchEngineModified(engine, data); + gSearchPane._updateSuggestionCheckboxes(); + break; + } + default: + this._engineStore.browserSearchEngineModified(engine, data); + } } } }, - 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; - }, - async setDefaultEngine() { await Services.search.setDefault( document.getElementById("defaultEngine").selectedItem.engine, @@ -840,92 +537,162 @@ var gSearchPane = { }, }; -function onDragEngineStart(event) { - var selectedIndex = gEngineView.selectedIndex; +/** + * Keeps track of the search engine objects and notifies the views for updates. + */ +class EngineStore { + /** + * A list of engines that are currently visible in the UI. + * + * @type {Object[]} + */ + engines = []; - // Local shortcut rows can't be dragged or re-ordered. - if (gEngineView._getLocalShortcut(selectedIndex)) { - event.preventDefault(); - return; - } + /** + * A list of application provided engines used when restoring the list of + * engines to the default set and order. + * + * @type {nsISearchEngine[]} + */ + #appProvidedEngines = []; - 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"; - } -} + /** + * A list of listeners to be notified when the engine list changes. + * + * @type {Object[]} + */ + #listeners = []; -function EngineStore() { - this._engines = []; - this._defaultEngines = []; - Promise.all([ - Services.search.getVisibleEngines(), - Services.search.getAppProvidedEngines(), - ]).then(([visibleEngines, defaultEngines]) => { + async init() { + let visibleEngines = await Services.search.getVisibleEngines(); 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, + let appProvidedEngines = await Services.search.getAppProvidedEngines(); + this.#appProvidedEngines = appProvidedEngines.map(this._cloneEngine, this); - get engines() { - return this._engines; - }, - set engines(val) { - this._engines = val; - }, + this.notifyRowCountChanged(0, visibleEngines.length); - _getIndexForEngine(aEngine) { - return this._engines.indexOf(aEngine); - }, + // check if we need to disable the restore defaults button + var someHidden = this.#appProvidedEngines.some(e => e.hidden); + gSearchPane.showRestoreDefaults(someHidden); + } - _getEngineByName(aName) { - return this._engines.find(engine => engine.name == aName); - }, + /** + * Adds a listener to be notified when the engine list changes. + * + * @param {object} aListener + */ + addListener(aListener) { + this.#listeners.push(aListener); + } + + /** + * Notifies all listeners that the engine list has changed and they should + * rebuild. + */ + notifyRebuildViews() { + for (let listener of this.#listeners) { + try { + listener.rebuild(this.engines); + } catch (ex) { + console.error("Error notifying EngineStore listener", ex); + } + } + } + + /** + * Notifies all listeners that the number of engines in the list has changed. + * + * @param {number} index + * @param {number} count + */ + notifyRowCountChanged(index, count) { + for (let listener of this.#listeners) { + listener.rowCountChanged(index, count, this.engines); + } + } + + /** + * Notifies all listeners that the default engine has changed. + * + * @param {string} type + * @param {object} engine + */ + notifyDefaultEngineChanged(type, engine) { + for (let listener of this.#listeners) { + if ("defaultEngineChanged" in listener) { + listener.defaultEngineChanged(type, engine, this.engines); + } + } + } + + notifyEngineIconUpdated(engine) { + // Check the engine is still in the list. + let index = this._getIndexForEngine(engine); + if (index != -1) { + for (let listener of this.#listeners) { + listener.engineIconUpdated(index, this.engines); + } + } + } + + _getIndexForEngine(aEngine) { + return this.engines.indexOf(aEngine); + } + + _getEngineByName(aName) { + return this.engines.find(engine => engine.name == aName); + } _cloneEngine(aEngine) { var clonedObj = { - iconURL: aEngine.getIconURL(), + iconURL: null, }; for (let i of ["id", "name", "alias", "hidden"]) { clonedObj[i] = aEngine[i]; } clonedObj.originalEngine = aEngine; + + // Trigger getting the iconURL for this engine. + aEngine.getIconURL().then(iconURL => { + if (iconURL) { + clonedObj.iconURL = iconURL; + } else if (window.devicePixelRatio > 1) { + clonedObj.iconURL = + "chrome://browser/skin/search-engine-placeholder@2x.png"; + } else { + clonedObj.iconURL = + "chrome://browser/skin/search-engine-placeholder.png"; + } + + this.notifyEngineIconUpdated(clonedObj); + }); + 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)); - }, + this.engines.push(this._cloneEngine(aEngine)); + } updateEngine(newEngine) { - let engineToUpdate = this._engines.findIndex( + 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) { + if (aNewIndex < 0 || aNewIndex > this.engines.length - 1) { throw new Error("ES_moveEngine: invalid aNewIndex!"); } var index = this._getIndexForEngine(aEngine); @@ -938,41 +705,73 @@ EngineStore.prototype = { } // nothing to do // Move the engine in our internal store - var removedEngine = this._engines.splice(index, 1)[0]; - this._engines.splice(aNewIndex, 0, removedEngine); + 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) { + 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); + let index = this.engines.findIndex(element => element.name == engineName); if (index == -1) { throw new Error("invalid engine?"); } - this._engines.splice(index, 1)[0]; + this.engines.splice(index, 1)[0]; if (aEngine.isAppProvided) { gSearchPane.showRestoreDefaults(true); } - gSearchPane.buildDefaultEngineDropDowns(); - return index; - }, + + this.notifyRowCountChanged(index, -1); + + document.getElementById("engineList").focus(); + } + + /** + * Update the default engine UI and engine tree view as appropriate when engine changes + * or locale changes occur. + * + * @param {nsISearchEngine} engine + * @param {string} data + */ + browserSearchEngineModified(engine, data) { + engine.QueryInterface(Ci.nsISearchEngine); + switch (data) { + case "engine-added": + this.addEngine(engine); + this.notifyRowCountChanged(gEngineView.lastEngineIndex, 1); + break; + case "engine-changed": + this.updateEngine(engine); + this.notifyRebuildViews(); + break; + case "engine-removed": + this.removeEngine(engine); + break; + case "engine-default": + this.notifyDefaultEngineChanged("normal", engine); + break; + case "engine-default-private": + this.notifyDefaultEngineChanged("private", engine); + break; + } + } async restoreDefaultEngines() { var added = 0; - for (var i = 0; i < this._defaultEngines.length; ++i) { - var e = this._defaultEngines[i]; + for (var i = 0; i < this.#appProvidedEngines.length; ++i) { + var e = this.#appProvidedEngines[i]; // If the engine is already in the list, just move it. - if (this._engines.some(this._isSameEngine, e)) { + if (this.engines.some(this._isSameEngine, e)) { await this.moveEngine(this._getEngineByName(e.name), i); } else { // Otherwise, add it back to our internal store @@ -981,7 +780,7 @@ EngineStore.prototype = { // so clear any alias we may have cached before unhiding the engine. e.alias = ""; - this._engines.splice(i, 0, e); + this.engines.splice(i, 0, e); let engine = e.originalEngine; engine.hidden = false; await Services.search.moveEngine(engine, i); @@ -1006,9 +805,9 @@ EngineStore.prototype = { Services.search.resetToAppDefaultEngine(); gSearchPane.showRestoreDefaults(false); - gSearchPane.buildDefaultEngineDropDowns(); + this.notifyRebuildViews(); return added; - }, + } changeEngine(aEngine, aProp, aNewValue) { var index = this._getIndexForEngine(aEngine); @@ -1016,22 +815,31 @@ EngineStore.prototype = { throw new Error("invalid engine?"); } - this._engines[index][aProp] = aNewValue; + this.engines[index][aProp] = aNewValue; aEngine.originalEngine[aProp] = aNewValue; - }, -}; - -function EngineView(aEngineStore) { - this._engineStore = aEngineStore; - - UrlbarPrefs.addObserver(this); - - this.loadL10nNames(); + } } -EngineView.prototype = { - _engineStore: null, - tree: null, +/** + * Manages the view of the Search Shortcuts tree on the search pane of preferences. + */ +class EngineView { + _engineStore = null; + _engineList = null; + tree = null; + + constructor(aEngineStore) { + this._engineStore = aEngineStore; + this._engineList = document.getElementById("engineList"); + this._engineList.view = this; + + UrlbarPrefs.addObserver(this); + aEngineStore.addListener(this); + + this.loadL10nNames(); + this.#addListeners(); + this.#showAddEngineButton(); + } loadL10nNames() { // This maps local shortcut sources to their l10n names. The names are needed @@ -1053,11 +861,33 @@ EngineView.prototype = { // called before name retrieval finished. this.invalidate(); }); - }, + } + + #addListeners() { + this._engineList.addEventListener("click", this); + this._engineList.addEventListener("dragstart", this); + this._engineList.addEventListener("keypress", this); + this._engineList.addEventListener("select", this); + this._engineList.addEventListener("dblclick", this); + } + + /** + * Shows the "Add Search Engine" button if the pref is enabled. + */ + #showAddEngineButton() { + let aliasRefresh = Services.prefs.getBoolPref( + "browser.urlbar.update2.engineAliasRefresh", + false + ); + if (aliasRefresh) { + let addButton = document.getElementById("addEngineButton"); + addButton.hidden = false; + } + } get lastEngineIndex() { return this._engineStore.engines.length - 1; - }, + } get selectedIndex() { var seln = this.selection; @@ -1067,34 +897,52 @@ EngineView.prototype = { return min.value; } return -1; - }, + } get selectedEngine() { return this._engineStore.engines[this.selectedIndex]; - }, + } // Helpers + rebuild() { + this.invalidate(); + } + rowCountChanged(index, count) { - if (this.tree) { - this.tree.rowCountChanged(index, count); + if (!this.tree) { + return; } - }, + this.tree.rowCountChanged(index, count); + + // If we're removing elements, ensure that we still have a selection. + if (count < 0) { + this.selection.select(Math.min(index, this.rowCount - 1)); + this.ensureRowIsVisible(this.currentIndex); + } + } + + engineIconUpdated(index) { + this.tree?.invalidateCell( + index, + this.tree.columns.getNamedColumn("engineName") + ); + } 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; @@ -1109,7 +957,7 @@ EngineView.prototype = { this.selectedEngine.name != defaultEngine.name && this.selectedEngine.name != defaultPrivateEngine.name ); - }, + } /** * Returns the local shortcut corresponding to a tree row, or null if the row @@ -1126,7 +974,7 @@ EngineView.prototype = { return null; } return UrlbarUtils.LOCAL_SEARCH_MODES[index - engineCount]; - }, + } /** * Called by UrlbarPrefs when a urlbar pref changes. @@ -1141,14 +989,170 @@ EngineView.prototype = { if (parts[0] == "shortcuts" && parts[1] && parts.length == 2) { this.invalidate(); } - }, + } + + 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(this.selectedIndex); + } + } + break; + case "click": + if ( + aEvent.target.id != "engineChildren" && + !aEvent.target.classList.contains("searchEngineAction") + ) { + // 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 (this._engineList.inputField.hidden && this._engineList.view) { + let selection = this._engineList.view.selection; + if (selection?.count > 0) { + selection.toggleSelect(selection.currentIndex); + } + this._engineList.blur(); + } + } + break; + case "command": + switch (aEvent.target.id) { + case "restoreDefaultSearchEngines": + this.#onRestoreDefaults(); + break; + case "removeEngineButton": + Services.search.removeEngine(this.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") { + this.#onDragEngineStart(aEvent); + } + break; + case "keypress": + if (aEvent.target.id == "engineList") { + this.#onTreeKeyPress(aEvent); + } + break; + case "select": + if (aEvent.target.id == "engineList") { + this.#onTreeSelect(); + } + break; + } + } + + /** + * Called when the restore default engines button is clicked to reset the + * list of engines to their defaults. + */ + async #onRestoreDefaults() { + let num = await this._engineStore.restoreDefaultEngines(); + this.rowCountChanged(0, num); + } + + #onDragEngineStart(event) { + let selectedIndex = this.selectedIndex; + + // Local shortcut rows can't be dragged or re-ordered. + if (this._getLocalShortcut(selectedIndex)) { + event.preventDefault(); + return; + } + + let tree = document.getElementById("engineList"); + let cell = tree.getCellAt(event.clientX, event.clientY); + if (selectedIndex >= 0 && !this.isCheckBox(cell.row, cell.col)) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } + } + + #onTreeSelect() { + document.getElementById("removeEngineButton").disabled = + !this.isEngineSelectedAndRemovable(); + } + + #onTreeKeyPress(aEvent) { + let index = this.selectedIndex; + let tree = document.getElementById("engineList"); + if (tree.hasAttribute("editing")) { + return; + } + + if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) { + // Space toggles the checkbox. + let newValue = !this.getCellValue( + index, + tree.columns.getNamedColumn("engineShown") + ); + this.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 && + this.isEngineSelectedAndRemovable()) + ) { + // Delete and Shift+Backspace (Mac) removes selected engine. + Services.search.removeEngine(this.selectedEngine.originalEngine); + } + } + } + + /** + * Triggers editing of an alias in the tree. + * + * @param {number} index + */ + #startEditingAlias(index) { + // Local shortcut aliases can't be edited. + if (this._getLocalShortcut(index)) { + return; + } + + let tree = document.getElementById("engineList"); + let engine = this._engineStore.engines[index]; + tree.startEditing(index, tree.columns.getLastColumn()); + tree.inputField.value = engine.alias || ""; + tree.inputField.select(); + } // nsITreeView get rowCount() { return ( this._engineStore.engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length ); - }, + } getImageSrc(index, column) { if (column.id == "engineName") { @@ -1157,18 +1161,11 @@ EngineView.prototype = { return shortcut.icon; } - if (this._engineStore.engines[index].iconURL) { - return this._engineStore.engines[index].iconURL; - } - - if (window.devicePixelRatio > 1) { - return "chrome://browser/skin/search-engine-placeholder@2x.png"; - } - return "chrome://browser/skin/search-engine-placeholder.png"; + return this._engineStore.engines[index].iconURL; } return ""; - }, + } getCellText(index, column) { if (column.id == "engineName") { @@ -1185,11 +1182,11 @@ EngineView.prototype = { return this._engineStore.engines[index].originalEngine.aliases.join(", "); } return ""; - }, + } setTree(tree) { this.tree = tree; - }, + } canDrop(targetIndex, orientation, dataTransfer) { var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); @@ -1200,7 +1197,7 @@ EngineView.prototype = { // 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 @@ -1223,17 +1220,16 @@ EngineView.prototype = { 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) { + selection = null; + getRowProperties() { return ""; - }, + } getCellProperties(index, column) { if (column.id == "engineName") { // For local shortcut rows, return the result source name so we can style @@ -1244,34 +1240,34 @@ EngineView.prototype = { } } return ""; - }, - getColumnProperties(column) { + } + getColumnProperties() { return ""; - }, - isContainer(index) { + } + isContainer() { return false; - }, - isContainerOpen(index) { + } + isContainerOpen() { return false; - }, - isContainerEmpty(index) { + } + isContainerEmpty() { return false; - }, - isSeparator(index) { + } + isSeparator() { return false; - }, - isSorted(index) { + } + isSorted() { return false; - }, - getParentIndex(index) { + } + getParentIndex() { return -1; - }, - hasNextSibling(parentIndex, index) { + } + hasNextSibling() { return false; - }, - getLevel(index) { + } + getLevel() { return 0; - }, + } getCellValue(index, column) { if (column.id == "engineShown") { let shortcut = this._getLocalShortcut(index); @@ -1281,17 +1277,17 @@ EngineView.prototype = { return !this._engineStore.engines[index].originalEngine.hideOneOffButton; } return undefined; - }, - toggleOpenState(index) {}, - cycleHeader(column) {}, - selectionChanged() {}, - cycleCell(row, column) {}, + } + toggleOpenState() {} + cycleHeader() {} + selectionChanged() {} + cycleCell() {} 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); @@ -1302,18 +1298,153 @@ EngineView.prototype = { } this._engineStore.engines[index].originalEngine.hideOneOffButton = value != "true"; - gEngineView.invalidate(); + this.invalidate(); } - }, + } setCellText(index, column, value) { if (column.id == "engineKeyword") { - gSearchPane - .editKeyword(this._engineStore.engines[index], value) - .then(valid => { + this.#changeKeyword(this._engineStore.engines[index], value).then( + valid => { if (!valid) { - gSearchPane.startEditingAlias(index); + this.#startEditingAlias(index); } - }); + } + ); } - }, -}; + } + + /** + * Handles changing the keyword for an engine. This will check for potentially + * duplicate keywords and prompt the user if necessary. + * + * @param {object} aEngine + * The engine to change. + * @param {string} aNewKeyword + * The new keyword. + * @returns {Promise} + * Resolves to true if the keyword was changed. + */ + async #changeKeyword(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 = this._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; + } + } + + this._engineStore.changeEngine(aEngine, "alias", keyword); + this.invalidate(); + return true; + } +} + +/** + * Manages the default engine dropdown buttons in the search pane of preferences. + */ +class DefaultEngineDropDown { + #element = null; + #type = null; + + constructor(type, engineStore) { + this.#type = type; + this.#element = document.getElementById( + type == "private" ? "defaultPrivateEngine" : "defaultEngine" + ); + + engineStore.addListener(this); + } + + rowCountChanged(index, count, enginesList) { + // Simply rebuild the menulist, rather than trying to update the changed row. + this.rebuild(enginesList); + } + + defaultEngineChanged(type, engine, enginesList) { + if (type != this.#type) { + return; + } + // 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 selectedEngineName = this.#element.selectedItem?.engine?.name; + if (selectedEngineName != engine.name) { + this.rebuild(enginesList); + } + } + + engineIconUpdated(index, enginesList) { + let item = this.#element.getItemAtIndex(index); + // Check this is the right item. + if (item?.label == enginesList[index].name) { + item.setAttribute("image", enginesList[index].iconURL); + } + } + + async rebuild(enginesList) { + if ( + this.#type == "private" && + !gSearchPane._separatePrivateDefaultPref.value + ) { + return; + } + let defaultEngine = await Services.search[ + this.#type == "normal" ? "getDefault" : "getDefaultPrivate" + ](); + + this.#element.removeAllItems(); + for (let engine of enginesList) { + let item = this.#element.appendItem(engine.name); + item.setAttribute( + "class", + "menuitem-iconic searchengine-menuitem menuitem-with-favicon" + ); + if (engine.iconURL) { + item.setAttribute("image", engine.iconURL); + } + item.engine = engine; + if (engine.name == defaultEngine.name) { + this.#element.selectedItem = item; + } + } + // This should never happen, but try and make sure we have at least one + // selected item. + if (!this.#element.selectedItem) { + this.#element.selectedIndex = 0; + } + } +} -- cgit v1.2.3