Bug 1884181 - Move EngineStore/EngineView functions out of gSearchPane and into the relevant class. r=mconley

Differential Revision: https://phabricator.services.mozilla.com/D204022
This commit is contained in:
Mark Banner
2024-03-12 13:38:14 +00:00
parent 7bc3786c4f
commit 29f80a5587

View File

@@ -38,11 +38,13 @@ const SEARCH_KEY = "defaultSearch";
var gEngineView = null;
var gSearchPane = {
init() {
let engineStore = new EngineStore();
gEngineView = new EngineView(engineStore);
_engineStore: null,
engineStore.init().catch(console.error);
init() {
this._engineStore = new EngineStore();
gEngineView = new EngineView(this._engineStore);
this._engineStore.init().catch(console.error);
if (
Services.policies &&
@@ -54,12 +56,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");
@@ -114,7 +111,6 @@ var gSearchPane = {
this._initDefaultEngines();
this._initShowSearchTermsCheckbox();
this._updateSuggestionCheckboxes();
this._showAddEngineButton();
this._initRecentSeachesCheckbox();
this._initAddressBar();
},
@@ -255,17 +251,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"
@@ -497,7 +482,7 @@ var gSearchPane = {
*/
async _buildEngineDropDown(list, currentEngine) {
// If the current engine isn't in the list any more, select the first item.
let engines = gEngineView._engineStore._engines;
let engines = this._engineStore._engines;
if (!engines.length) {
return;
}
@@ -507,7 +492,7 @@ var gSearchPane = {
// Now clean-up and rebuild the list.
list.removeAllItems();
gEngineView._engineStore._engines.forEach(e => {
this._engineStore._engines.forEach(e => {
let item = list.appendItem(e.name);
item.setAttribute(
"class",
@@ -524,86 +509,23 @@ var gSearchPane = {
},
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);
if (aEvent.type != "command") {
return;
}
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);
}
},
@@ -615,60 +537,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.
*/
@@ -679,144 +547,48 @@ var gSearchPane = {
break;
}
case "browser-search-engine-modified": {
this.browserSearchEngineModified(subject, data);
break;
let engine = subject.QueryInterface(Ci.nsISearchEngine);
switch (data) {
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;
}
default:
this._engineStore.browserSearchEngineModified(subject, data);
}
}
}
},
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;
},
async setDefaultEngine() {
await Services.search.setDefault(
document.getElementById("defaultEngine").selectedItem.engine,
@@ -839,23 +611,6 @@ var gSearchPane = {
},
};
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";
}
}
class EngineStore {
_engines = [];
/**
@@ -964,8 +719,47 @@ class EngineStore {
if (aEngine.isAppProvided) {
gSearchPane.showRestoreDefaults(true);
}
gSearchPane.buildDefaultEngineDropDowns();
return index;
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();
}
/**
* 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":
this.addEngine(engine);
gEngineView.rowCountChanged(gEngineView.lastEngineIndex, 1);
gSearchPane.buildDefaultEngineDropDowns();
break;
case "engine-changed":
gSearchPane.buildDefaultEngineDropDowns();
this.updateEngine(engine);
gEngineView.invalidate();
break;
case "engine-removed":
this.removeEngine(engine);
break;
}
}
async restoreDefaultEngines() {
@@ -1025,18 +819,22 @@ class EngineStore {
}
class EngineView {
_engineStore = null;
_engineList = null;
tree = null;
constructor(aEngineStore) {
this._engineStore = aEngineStore;
document.getElementById("engineList").view = this;
this._engineList = document.getElementById("engineList");
this._engineList.view = this;
UrlbarPrefs.addObserver(this);
this.loadL10nNames();
this.#addListeners();
this.#showAddEngineButton();
}
_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
@@ -1059,6 +857,28 @@ class EngineView {
});
}
#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;
}
@@ -1147,6 +967,163 @@ class EngineView {
}
}
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);
this.invalidate();
}
#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 (
@@ -1306,18 +1283,77 @@ class EngineView {
}
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;
}
}