fune/browser/components/preferences/search.js
James Teow d8d5267819 Bug 1800851 - showSearchTerms checkbox should listen to search bar changes - r=adw,settings-reviewers
While trying to simplify the test code prior to landing the initial preference
panel update, I removed some checks in the test that ensured the checkbox
was actually being checked if the user enabled the search bar while the search
panel was opened. Hence how the tests passed despite the fact that I neglected
to include a listener.

Differential Revision: https://phabricator.services.mozilla.com/D162583
2022-11-22 18:20:22 +00:00

1098 lines
33 KiB
JavaScript

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