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.js1113
1 files changed, 622 insertions, 491 deletions
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);
+ 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 "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;
+ default:
+ gEngineView.handleEvent(aEvent);
}
},
@@ -617,60 +487,6 @@ var gSearchPane = {
},
/**
- * 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.
*/
observe(subject, topic, data) {
@@ -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();
+
+ let appProvidedEngines = await Services.search.getAppProvidedEngines();
+ this.#appProvidedEngines = appProvidedEngines.map(this._cloneEngine, this);
+
+ this.notifyRowCountChanged(0, visibleEngines.length);
// check if we need to disable the restore defaults button
- var someHidden = this._defaultEngines.some(e => e.hidden);
+ var someHidden = this.#appProvidedEngines.some(e => e.hidden);
gSearchPane.showRestoreDefaults(someHidden);
- });
-}
-EngineStore.prototype = {
- _engines: null,
- _defaultEngines: null,
+ }
- get engines() {
- return this._engines;
- },
- set engines(val) {
- this._engines = val;
- },
+ /**
+ * 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);
- },
+ return this.engines.indexOf(aEngine);
+ }
_getEngineByName(aName) {
- return this._engines.find(engine => engine.name == 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 <window> 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<boolean>}
+ * 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;
+ }
+ }
+}