1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* import-globals-from extensionControlled.js */
6 /* import-globals-from preferences.js */
10 ChromeUtils
.defineESModuleGetters(lazy
, {
11 SearchUIUtils
: "resource:///modules/SearchUIUtils.sys.mjs",
12 SearchUtils
: "resource://gre/modules/SearchUtils.sys.mjs",
13 UrlbarPrefs
: "resource:///modules/UrlbarPrefs.sys.mjs",
14 CustomizableUI
: "resource:///modules/CustomizableUI.sys.mjs",
17 const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST
=
18 "browser.urlbar.quicksuggest.blockedDigests";
19 const PREF_URLBAR_WEATHER_USER_ENABLED
= "browser.urlbar.suggest.weather";
22 { id
: "browser.search.suggest.enabled", type
: "bool" },
23 { id
: "browser.urlbar.suggest.searches", type
: "bool" },
24 { id
: "browser.search.suggest.enabled.private", type
: "bool" },
25 { id
: "browser.urlbar.showSearchSuggestionsFirst", type
: "bool" },
26 { id
: "browser.urlbar.showSearchTerms.enabled", type
: "bool" },
27 { id
: "browser.search.separatePrivateDefault", type
: "bool" },
28 { id
: "browser.search.separatePrivateDefault.ui.enabled", type
: "bool" },
29 { id
: "browser.urlbar.suggest.trending", type
: "bool" },
30 { id
: "browser.urlbar.trending.featureGate", type
: "bool" },
31 { id
: "browser.urlbar.recentsearches.featureGate", type
: "bool" },
32 { id
: "browser.urlbar.suggest.recentsearches", type
: "bool" },
33 { id
: "browser.urlbar.scotchBonnet.enableOverride", type
: "bool" },
36 const ENGINE_FLAVOR
= "text/x-moz-search-engine";
37 const SEARCH_TYPE
= "default_search";
38 const SEARCH_KEY
= "defaultSearch";
40 var gEngineView
= null;
44 _engineDropDown
: null,
45 _engineDropDownPrivate
: null,
48 this._engineStore
= new EngineStore();
49 gEngineView
= new EngineView(this._engineStore
);
51 this._engineDropDown
= new DefaultEngineDropDown(
55 this._engineDropDownPrivate
= new DefaultEngineDropDown(
60 this._engineStore
.init().catch(console
.error
);
64 !Services
.policies
.isAllowed("installSearchEngine")
66 document
.getElementById("addEnginesBox").hidden
= true;
68 let addEnginesLink
= document
.getElementById("addEngines");
69 addEnginesLink
.setAttribute("href", lazy
.SearchUIUtils
.searchEnginesURL
);
72 window
.addEventListener("command", this);
74 Services
.obs
.addObserver(this, "browser-search-engine-modified");
75 Services
.obs
.addObserver(this, "intl:app-locales-changed");
76 window
.addEventListener("unload", () => {
77 Services
.obs
.removeObserver(this, "browser-search-engine-modified");
78 Services
.obs
.removeObserver(this, "intl:app-locales-changed");
81 let suggestsPref
= Preferences
.get("browser.search.suggest.enabled");
82 let urlbarSuggestsPref
= Preferences
.get("browser.urlbar.suggest.searches");
83 let privateSuggestsPref
= Preferences
.get(
84 "browser.search.suggest.enabled.private"
87 let updateSuggestionCheckboxes
=
88 this._updateSuggestionCheckboxes
.bind(this);
89 suggestsPref
.on("change", updateSuggestionCheckboxes
);
90 urlbarSuggestsPref
.on("change", updateSuggestionCheckboxes
);
91 let customizableUIListener
= {
92 onWidgetAfterDOMChange
: node
=> {
93 if (node
.id
== "search-container") {
94 updateSuggestionCheckboxes();
98 lazy
.CustomizableUI
.addListener(customizableUIListener
);
99 window
.addEventListener("unload", () => {
100 lazy
.CustomizableUI
.removeListener(customizableUIListener
);
103 let urlbarSuggests
= document
.getElementById("urlBarSuggestion");
104 urlbarSuggests
.addEventListener("command", () => {
105 urlbarSuggestsPref
.value
= urlbarSuggests
.checked
;
107 let suggestionsInSearchFieldsCheckbox
= document
.getElementById(
108 "suggestionsInSearchFieldsCheckbox"
110 // We only want to call _updateSuggestionCheckboxes once after updating
112 suggestionsInSearchFieldsCheckbox
.addEventListener("command", () => {
113 this._skipUpdateSuggestionCheckboxesFromPrefChanges
= true;
114 if (!lazy
.CustomizableUI
.getPlacementOfWidget("search-container")) {
115 urlbarSuggestsPref
.value
= suggestionsInSearchFieldsCheckbox
.checked
;
117 suggestsPref
.value
= suggestionsInSearchFieldsCheckbox
.checked
;
118 this._skipUpdateSuggestionCheckboxesFromPrefChanges
= false;
119 this._updateSuggestionCheckboxes();
121 let privateWindowCheckbox
= document
.getElementById(
122 "showSearchSuggestionsPrivateWindows"
124 privateWindowCheckbox
.addEventListener("command", () => {
125 privateSuggestsPref
.value
= privateWindowCheckbox
.checked
;
129 "browserSeparateDefaultEngine",
131 this._onBrowserSeparateDefaultEngineChange
.bind(this)
134 this._initDefaultEngines();
135 this._initShowSearchTermsCheckbox();
136 this._updateSuggestionCheckboxes();
137 this._initRecentSeachesCheckbox();
138 this._initAddressBar();
142 * Initialize the default engine handling. This will hide the private default
143 * options if they are not enabled yet.
145 _initDefaultEngines() {
146 this._separatePrivateDefaultEnabledPref
= Preferences
.get(
147 "browser.search.separatePrivateDefault.ui.enabled"
150 this._separatePrivateDefaultPref
= Preferences
.get(
151 "browser.search.separatePrivateDefault"
154 const checkbox
= document
.getElementById("browserSeparateDefaultEngine");
155 checkbox
.checked
= !this._separatePrivateDefaultPref
.value
;
157 this._updatePrivateEngineDisplayBoxes();
159 const listener
= () => {
160 this._updatePrivateEngineDisplayBoxes();
161 this._engineStore
.notifyRebuildViews();
164 this._separatePrivateDefaultEnabledPref
.on("change", listener
);
165 this._separatePrivateDefaultPref
.on("change", listener
);
168 _initShowSearchTermsCheckbox() {
169 let checkbox
= document
.getElementById("searchShowSearchTermCheckbox");
170 let updateCheckboxHidden
= () => {
172 !UrlbarPrefs
.getScotchBonnetPref("showSearchTerms.featureGate") ||
173 !!lazy
.CustomizableUI
.getPlacementOfWidget("search-container");
176 // Add observer of CustomizableUI as showSearchTerms checkbox
177 // should be hidden while Search Bar is enabled.
178 let customizableUIListener
= {
179 onWidgetAfterDOMChange
: node
=> {
180 if (node
.id
== "search-container") {
181 updateCheckboxHidden();
185 lazy
.CustomizableUI
.addListener(customizableUIListener
);
187 // Fire once to initialize.
188 updateCheckboxHidden();
190 window
.addEventListener("unload", () => {
191 lazy
.CustomizableUI
.removeListener(customizableUIListener
);
195 _updatePrivateEngineDisplayBoxes() {
196 const separateEnabled
= this._separatePrivateDefaultEnabledPref
.value
;
197 document
.getElementById("browserSeparateDefaultEngine").hidden
=
200 const separateDefault
= this._separatePrivateDefaultPref
.value
;
202 const vbox
= document
.getElementById("browserPrivateEngineSelection");
203 vbox
.hidden
= !separateEnabled
|| !separateDefault
;
206 _onBrowserSeparateDefaultEngineChange(event
) {
207 this._separatePrivateDefaultPref
.value
= !event
.target
.checked
;
210 _updateSuggestionCheckboxes() {
211 if (this._skipUpdateSuggestionCheckboxesFromPrefChanges
) {
214 let suggestsPref
= Preferences
.get("browser.search.suggest.enabled");
215 let permanentPB
= Services
.prefs
.getBoolPref(
216 "browser.privatebrowsing.autostart"
218 let urlbarSuggests
= document
.getElementById("urlBarSuggestion");
219 let suggestionsInSearchFieldsCheckbox
= document
.getElementById(
220 "suggestionsInSearchFieldsCheckbox"
222 let positionCheckbox
= document
.getElementById(
223 "showSearchSuggestionsFirstCheckbox"
225 let privateWindowCheckbox
= document
.getElementById(
226 "showSearchSuggestionsPrivateWindows"
228 let urlbarSuggestsPref
= Preferences
.get("browser.urlbar.suggest.searches");
229 let searchBarVisible
=
230 !!lazy
.CustomizableUI
.getPlacementOfWidget("search-container");
232 suggestionsInSearchFieldsCheckbox
.checked
=
233 suggestsPref
.value
&& (searchBarVisible
|| urlbarSuggestsPref
.value
);
235 urlbarSuggests
.disabled
= !suggestsPref
.value
|| permanentPB
;
236 urlbarSuggests
.hidden
= !searchBarVisible
;
238 privateWindowCheckbox
.disabled
= !suggestsPref
.value
;
239 privateWindowCheckbox
.checked
= Preferences
.get(
240 "browser.search.suggest.enabled.private"
242 if (privateWindowCheckbox
.disabled
) {
243 privateWindowCheckbox
.checked
= false;
246 urlbarSuggests
.checked
= urlbarSuggestsPref
.value
;
247 if (urlbarSuggests
.disabled
) {
248 urlbarSuggests
.checked
= false;
250 if (urlbarSuggests
.checked
) {
251 positionCheckbox
.disabled
= false;
252 // Update the checked state of the show-suggestions-first checkbox. Note
253 // that this does *not* also update its pref, it only checks the box.
254 positionCheckbox
.checked
= Preferences
.get(
255 positionCheckbox
.getAttribute("preference")
258 positionCheckbox
.disabled
= true;
259 positionCheckbox
.checked
= false;
262 suggestionsInSearchFieldsCheckbox
.checked
&&
264 !urlbarSuggests
.checked
266 urlbarSuggestsPref
.value
= true;
269 let permanentPBLabel
= document
.getElementById(
270 "urlBarSuggestionPermanentPBLabel"
272 permanentPBLabel
.hidden
= urlbarSuggests
.hidden
|| !permanentPB
;
274 this._updateTrendingCheckbox(!suggestsPref
.value
|| permanentPB
);
277 _initRecentSeachesCheckbox() {
278 this._recentSearchesEnabledPref
= Preferences
.get(
279 "browser.urlbar.recentsearches.featureGate"
281 let recentSearchesCheckBox
= document
.getElementById(
282 "enableRecentSearches"
284 const listener
= () => {
285 recentSearchesCheckBox
.hidden
= !this._recentSearchesEnabledPref
.value
;
288 this._recentSearchesEnabledPref
.on("change", listener
);
292 async
_updateTrendingCheckbox(suggestDisabled
) {
293 let trendingBox
= document
.getElementById("showTrendingSuggestionsBox");
294 let trendingCheckBox
= document
.getElementById("showTrendingSuggestions");
295 let trendingSupported
= (
296 await Services
.search
.getDefault()
297 ).supportsResponseType(lazy
.SearchUtils
.URL_TYPE
.TRENDING_JSON
);
298 trendingBox
.hidden
= !Preferences
.get("browser.urlbar.trending.featureGate")
300 trendingCheckBox
.disabled
= suggestDisabled
|| !trendingSupported
;
306 * Initializes the address bar section.
309 // Update the Firefox Suggest section when its Nimbus config changes.
310 let onNimbus
= () => this._updateFirefoxSuggestSection();
311 NimbusFeatures
.urlbar
.onUpdate(onNimbus
);
312 window
.addEventListener("unload", () => {
313 NimbusFeatures
.urlbar
.offUpdate(onNimbus
);
316 // The Firefox Suggest info box potentially needs updating when any of the
319 "browser.urlbar.suggest.quicksuggest.nonsponsored",
320 "browser.urlbar.suggest.quicksuggest.sponsored",
321 "browser.urlbar.quicksuggest.dataCollection.enabled",
323 for (let pref
of infoBoxPrefs
) {
324 Preferences
.get(pref
).on("change", () =>
325 this._updateFirefoxSuggestInfoBox()
329 document
.getElementById("clipboardSuggestion").hidden
= !UrlbarPrefs
.get(
330 "clipboard.featureGate"
333 this._updateFirefoxSuggestSection(true);
334 this._initQuickActionsSection();
338 * Updates the Firefox Suggest section (in the address bar section) depending
339 * on whether the user is enrolled in a Firefox Suggest rollout.
341 * @param {boolean} [onInit]
342 * Pass true when calling this when initializing the pane.
344 _updateFirefoxSuggestSection(onInit
= false) {
345 let container
= document
.getElementById("firefoxSuggestContainer");
348 UrlbarPrefs
.get("quickSuggestEnabled") &&
349 !UrlbarPrefs
.get("quickSuggestHideSettingsUI")
351 // Update the l10n IDs of text elements.
352 let l10nIdByElementId
= {
353 locationBarGroupHeader
: "addressbar-header-firefox-suggest",
354 locationBarSuggestionLabel
: "addressbar-suggest-firefox-suggest",
356 for (let [elementId
, l10nId
] of Object
.entries(l10nIdByElementId
)) {
357 let element
= document
.getElementById(elementId
);
358 element
.dataset
.l10nIdOriginal
??= element
.dataset
.l10nId
;
359 element
.dataset
.l10nId
= l10nId
;
362 // Show the container.
363 this._updateFirefoxSuggestInfoBox();
365 this._updateDismissedSuggestionsStatus();
366 Preferences
.get(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST
).on("change", () =>
367 this._updateDismissedSuggestionsStatus()
369 Preferences
.get(PREF_URLBAR_WEATHER_USER_ENABLED
).on("change", () =>
370 this._updateDismissedSuggestionsStatus()
372 setEventListener("restoreDismissedSuggestions", "command", () =>
373 this.restoreDismissedSuggestions()
376 container
.removeAttribute("hidden");
377 } else if (!onInit
) {
378 // Firefox Suggest is not enabled. This is the default, so to avoid
379 // accidentally messing anything up, only modify the doc if we're being
380 // called due to a change in the rollout-enabled status (!onInit).
381 container
.setAttribute("hidden", "true");
382 let elementIds
= ["locationBarGroupHeader", "locationBarSuggestionLabel"];
383 for (let id
of elementIds
) {
384 let element
= document
.getElementById(id
);
385 if (element
.dataset
.l10nIdOriginal
) {
386 document
.l10n
.setAttributes(element
, element
.dataset
.l10nIdOriginal
);
387 delete element
.dataset
.l10nIdOriginal
;
394 * Updates the Firefox Suggest info box (in the address bar section) depending
395 * on the states of the Firefox Suggest toggles.
397 _updateFirefoxSuggestInfoBox() {
398 let nonsponsored
= Preferences
.get(
399 "browser.urlbar.suggest.quicksuggest.nonsponsored"
401 let sponsored
= Preferences
.get(
402 "browser.urlbar.suggest.quicksuggest.sponsored"
404 let dataCollection
= Preferences
.get(
405 "browser.urlbar.quicksuggest.dataCollection.enabled"
408 // Get the l10n ID of the appropriate text based on the values of the three
411 if (nonsponsored
&& sponsored
&& dataCollection
) {
412 l10nId
= "addressbar-firefox-suggest-info-all";
413 } else if (nonsponsored
&& sponsored
&& !dataCollection
) {
414 l10nId
= "addressbar-firefox-suggest-info-nonsponsored-sponsored";
415 } else if (nonsponsored
&& !sponsored
&& dataCollection
) {
416 l10nId
= "addressbar-firefox-suggest-info-nonsponsored-data";
417 } else if (nonsponsored
&& !sponsored
&& !dataCollection
) {
418 l10nId
= "addressbar-firefox-suggest-info-nonsponsored";
419 } else if (!nonsponsored
&& sponsored
&& dataCollection
) {
420 l10nId
= "addressbar-firefox-suggest-info-sponsored-data";
421 } else if (!nonsponsored
&& sponsored
&& !dataCollection
) {
422 l10nId
= "addressbar-firefox-suggest-info-sponsored";
423 } else if (!nonsponsored
&& !sponsored
&& dataCollection
) {
424 l10nId
= "addressbar-firefox-suggest-info-data";
427 let instance
= (this._firefoxSuggestInfoBoxInstance
= {});
428 let infoBox
= document
.getElementById("firefoxSuggestInfoBox");
430 infoBox
.hidden
= true;
432 let infoText
= document
.getElementById("firefoxSuggestInfoText");
433 infoText
.dataset
.l10nId
= l10nId
;
435 // If the info box is currently hidden and we unhide it immediately, it
436 // will show its old text until the new text is asyncly fetched and shown.
437 // That's ugly, so wait for the fetch to finish before unhiding it.
438 document
.l10n
.translateElements([infoText
]).then(() => {
439 if (instance
== this._firefoxSuggestInfoBoxInstance
) {
440 infoBox
.hidden
= false;
446 _initQuickActionsSection() {
447 let showPref
= Preferences
.get("browser.urlbar.quickactions.showPrefs");
448 let scotchBonnet
= Preferences
.get(
449 "browser.urlbar.scotchBonnet.enableOverride"
451 let showQuickActionsGroup
= () => {
452 document
.getElementById("quickActionsBox").hidden
= !(
453 showPref
.value
|| scotchBonnet
.value
456 showPref
.on("change", showQuickActionsGroup
);
457 showQuickActionsGroup();
461 * Enables/disables the "Restore" button for dismissed Firefox Suggest
464 _updateDismissedSuggestionsStatus() {
465 document
.getElementById("restoreDismissedSuggestions").disabled
=
466 !Services
.prefs
.prefHasUserValue(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST
) &&
468 Services
.prefs
.prefHasUserValue(PREF_URLBAR_WEATHER_USER_ENABLED
) &&
469 !Services
.prefs
.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED
)
474 * Restores Firefox Suggest suggestions dismissed by the user.
476 restoreDismissedSuggestions() {
477 Services
.prefs
.clearUserPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST
);
478 Services
.prefs
.clearUserPref(PREF_URLBAR_WEATHER_USER_ENABLED
);
481 handleEvent(aEvent
) {
482 if (aEvent
.type
!= "command") {
485 switch (aEvent
.target
.id
) {
487 if (aEvent
.target
.parentNode
&& aEvent
.target
.parentNode
.parentNode
) {
488 if (aEvent
.target
.parentNode
.parentNode
.id
== "defaultEngine") {
489 gSearchPane
.setDefaultEngine();
491 aEvent
.target
.parentNode
.parentNode
.id
== "defaultPrivateEngine"
493 gSearchPane
.setDefaultPrivateEngine();
498 gEngineView
.handleEvent(aEvent
);
503 * Handle when the app locale is changed.
505 async
appLocalesChanged() {
506 await document
.l10n
.ready
;
507 await gEngineView
.loadL10nNames();
511 * nsIObserver implementation.
513 observe(subject
, topic
, data
) {
515 case "intl:app-locales-changed": {
516 this.appLocalesChanged();
519 case "browser-search-engine-modified": {
520 let engine
= subject
.QueryInterface(Ci
.nsISearchEngine
);
522 case "engine-default": {
523 // Pass through to the engine store to handle updates.
524 this._engineStore
.browserSearchEngineModified(engine
, data
);
525 gSearchPane
._updateSuggestionCheckboxes();
529 this._engineStore
.browserSearchEngineModified(engine
, data
);
535 showRestoreDefaults(aEnable
) {
536 document
.getElementById("restoreDefaultSearchEngines").disabled
= !aEnable
;
539 async
setDefaultEngine() {
540 await Services
.search
.setDefault(
541 document
.getElementById("defaultEngine").selectedItem
.engine
,
542 Ci
.nsISearchService
.CHANGE_REASON_USER
544 if (ExtensionSettingsStore
.getSetting(SEARCH_TYPE
, SEARCH_KEY
) !== null) {
545 ExtensionSettingsStore
.select(
546 ExtensionSettingsStore
.SETTING_USER_SET
,
553 async
setDefaultPrivateEngine() {
554 await Services
.search
.setDefaultPrivate(
555 document
.getElementById("defaultPrivateEngine").selectedItem
.engine
,
556 Ci
.nsISearchService
.CHANGE_REASON_USER
562 * Keeps track of the search engine objects and notifies the views for updates.
566 * A list of engines that are currently visible in the UI.
573 * A list of application provided engines used when restoring the list of
574 * engines to the default set and order.
576 * @type {nsISearchEngine[]}
578 #appProvidedEngines
= [];
581 * A list of listeners to be notified when the engine list changes.
588 let visibleEngines
= await Services
.search
.getVisibleEngines();
589 for (let engine
of visibleEngines
) {
590 this.addEngine(engine
);
593 let appProvidedEngines
= await Services
.search
.getAppProvidedEngines();
594 this.#appProvidedEngines
= appProvidedEngines
.map(this._cloneEngine
, this);
596 this.notifyRowCountChanged(0, visibleEngines
.length
);
598 // check if we need to disable the restore defaults button
599 var someHidden
= this.#appProvidedEngines
.some(e
=> e
.hidden
);
600 gSearchPane
.showRestoreDefaults(someHidden
);
604 * Adds a listener to be notified when the engine list changes.
606 * @param {object} aListener
608 addListener(aListener
) {
609 this.#listeners
.push(aListener
);
613 * Notifies all listeners that the engine list has changed and they should
616 notifyRebuildViews() {
617 for (let listener
of this.#listeners
) {
619 listener
.rebuild(this.engines
);
621 console
.error("Error notifying EngineStore listener", ex
);
627 * Notifies all listeners that the number of engines in the list has changed.
629 * @param {number} index
630 * @param {number} count
632 notifyRowCountChanged(index
, count
) {
633 for (let listener
of this.#listeners
) {
634 listener
.rowCountChanged(index
, count
, this.engines
);
639 * Notifies all listeners that the default engine has changed.
641 * @param {string} type
642 * @param {object} engine
644 notifyDefaultEngineChanged(type
, engine
) {
645 for (let listener
of this.#listeners
) {
646 if ("defaultEngineChanged" in listener
) {
647 listener
.defaultEngineChanged(type
, engine
, this.engines
);
652 notifyEngineIconUpdated(engine
) {
653 // Check the engine is still in the list.
654 let index
= this._getIndexForEngine(engine
);
656 for (let listener
of this.#listeners
) {
657 listener
.engineIconUpdated(index
, this.engines
);
662 _getIndexForEngine(aEngine
) {
663 return this.engines
.indexOf(aEngine
);
666 _getEngineByName(aName
) {
667 return this.engines
.find(engine
=> engine
.name
== aName
);
670 _cloneEngine(aEngine
) {
674 for (let i
of ["id", "name", "alias", "hidden"]) {
675 clonedObj
[i
] = aEngine
[i
];
677 clonedObj
.originalEngine
= aEngine
;
679 // Trigger getting the iconURL for this engine.
680 aEngine
.getIconURL().then(iconURL
=> {
682 clonedObj
.iconURL
= iconURL
;
683 } else if (window
.devicePixelRatio
> 1) {
685 "chrome://browser/skin/search-engine-placeholder@2x.png";
688 "chrome://browser/skin/search-engine-placeholder.png";
691 this.notifyEngineIconUpdated(clonedObj
);
697 // Callback for Array's some(). A thisObj must be passed to some()
698 _isSameEngine(aEngineClone
) {
699 return aEngineClone
.originalEngine
.id
== this.originalEngine
.id
;
703 this.engines
.push(this._cloneEngine(aEngine
));
706 updateEngine(newEngine
) {
707 let engineToUpdate
= this.engines
.findIndex(
708 e
=> e
.originalEngine
.id
== newEngine
.id
710 if (engineToUpdate
!= -1) {
711 this.engines
[engineToUpdate
] = this._cloneEngine(newEngine
);
715 moveEngine(aEngine
, aNewIndex
) {
716 if (aNewIndex
< 0 || aNewIndex
> this.engines
.length
- 1) {
717 throw new Error("ES_moveEngine: invalid aNewIndex!");
719 var index
= this._getIndexForEngine(aEngine
);
721 throw new Error("ES_moveEngine: invalid engine?");
724 if (index
== aNewIndex
) {
725 return Promise
.resolve();
728 // Move the engine in our internal store
729 var removedEngine
= this.engines
.splice(index
, 1)[0];
730 this.engines
.splice(aNewIndex
, 0, removedEngine
);
732 return Services
.search
.moveEngine(aEngine
.originalEngine
, aNewIndex
);
735 removeEngine(aEngine
) {
736 if (this.engines
.length
== 1) {
737 throw new Error("Cannot remove last engine!");
740 let engineName
= aEngine
.name
;
741 let index
= this.engines
.findIndex(element
=> element
.name
== engineName
);
744 throw new Error("invalid engine?");
747 this.engines
.splice(index
, 1)[0];
749 if (aEngine
.isAppProvided
) {
750 gSearchPane
.showRestoreDefaults(true);
753 this.notifyRowCountChanged(index
, -1);
755 document
.getElementById("engineList").focus();
759 * Update the default engine UI and engine tree view as appropriate when engine changes
760 * or locale changes occur.
762 * @param {nsISearchEngine} engine
763 * @param {string} data
765 browserSearchEngineModified(engine
, data
) {
766 engine
.QueryInterface(Ci
.nsISearchEngine
);
769 this.addEngine(engine
);
770 this.notifyRowCountChanged(gEngineView
.lastEngineIndex
, 1);
772 case "engine-changed":
773 case "engine-icon-changed":
774 this.updateEngine(engine
);
775 this.notifyRebuildViews();
777 case "engine-removed":
778 this.removeEngine(engine
);
780 case "engine-default":
781 this.notifyDefaultEngineChanged("normal", engine
);
783 case "engine-default-private":
784 this.notifyDefaultEngineChanged("private", engine
);
789 async
restoreDefaultEngines() {
792 for (var i
= 0; i
< this.#appProvidedEngines
.length
; ++i
) {
793 var e
= this.#appProvidedEngines
[i
];
795 // If the engine is already in the list, just move it.
796 if (this.engines
.some(this._isSameEngine
, e
)) {
797 await
this.moveEngine(this._getEngineByName(e
.name
), i
);
799 // Otherwise, add it back to our internal store
801 // The search service removes the alias when an engine is hidden,
802 // so clear any alias we may have cached before unhiding the engine.
805 this.engines
.splice(i
, 0, e
);
806 let engine
= e
.originalEngine
;
807 engine
.hidden
= false;
808 await Services
.search
.moveEngine(engine
, i
);
813 // We can't do this as part of the loop above because the indices are
814 // used for moving engines.
815 let policyRemovedEngineNames
=
816 Services
.policies
.getActivePolicies()?.SearchEngines
?.Remove
|| [];
817 for (let engineName
of policyRemovedEngineNames
) {
818 let engine
= Services
.search
.getEngineByName(engineName
);
821 await Services
.search
.removeEngine(engine
);
823 // Engine might not exist
828 Services
.search
.resetToAppDefaultEngine();
829 gSearchPane
.showRestoreDefaults(false);
830 this.notifyRebuildViews();
834 changeEngine(aEngine
, aProp
, aNewValue
) {
835 var index
= this._getIndexForEngine(aEngine
);
837 throw new Error("invalid engine?");
840 this.engines
[index
][aProp
] = aNewValue
;
841 aEngine
.originalEngine
[aProp
] = aNewValue
;
846 * Manages the view of the Search Shortcuts tree on the search pane of preferences.
853 constructor(aEngineStore
) {
854 this._engineStore
= aEngineStore
;
855 this._engineList
= document
.getElementById("engineList");
856 this._engineList
.view
= this;
858 UrlbarPrefs
.addObserver(this);
859 aEngineStore
.addListener(this);
861 this.loadL10nNames();
862 this.#addListeners();
863 this.#showAddEngineButton();
866 async
loadL10nNames() {
867 // This maps local shortcut sources to their l10n names. The names are needed
868 // by getCellText. Getting the names is async but getCellText is not, so we
869 // cache them here to retrieve them syncronously in getCellText.
870 this._localShortcutL10nNames
= new Map();
872 let getIDs
= (suffix
= "") =>
873 UrlbarUtils
.LOCAL_SEARCH_MODES
.map(mode
=> {
874 let name
= UrlbarUtils
.getResultSourceName(mode
.source
);
875 return { id
: `urlbar-search-mode-${name}${suffix}` };
879 let localizedIDs
= getIDs();
880 let englishIDs
= getIDs("-en");
882 let englishSearchStrings
= new Localization([
883 "preview/enUS-searchFeatures.ftl",
885 let localizedNames
= await document
.l10n
.formatValues(localizedIDs
);
886 let englishNames
= await englishSearchStrings
.formatValues(englishIDs
);
888 UrlbarUtils
.LOCAL_SEARCH_MODES
.forEach(({ source
}, index
) => {
889 let localizedName
= localizedNames
[index
];
890 let englishName
= englishNames
[index
];
892 // Add only the English name if localized and English are the same
894 localizedName
=== englishName
896 : [localizedName
, englishName
];
898 this._localShortcutL10nNames
.set(source
, names
);
900 // Invalidate the tree now that we have the names in case getCellText was
901 // called before name retrieval finished.
905 console
.error("Error loading l10n names", ex
);
910 this._engineList
.addEventListener("click", this);
911 this._engineList
.addEventListener("dragstart", this);
912 this._engineList
.addEventListener("keypress", this);
913 this._engineList
.addEventListener("select", this);
914 this._engineList
.addEventListener("dblclick", this);
918 * Shows the "Add Search Engine" button if the pref is enabled.
920 #showAddEngineButton() {
921 let aliasRefresh
= Services
.prefs
.getBoolPref(
922 "browser.urlbar.update2.engineAliasRefresh",
926 let addButton
= document
.getElementById("addEngineButton");
927 addButton
.hidden
= false;
931 get lastEngineIndex() {
932 return this._engineStore
.engines
.length
- 1;
935 get selectedIndex() {
936 var seln
= this.selection
;
937 if (seln
.getRangeCount() > 0) {
939 seln
.getRangeAt(0, min
, {});
945 get selectedEngine() {
946 return this._engineStore
.engines
[this.selectedIndex
];
954 rowCountChanged(index
, count
) {
958 this.tree
.rowCountChanged(index
, count
);
960 // If we're removing elements, ensure that we still have a selection.
962 this.selection
.select(Math
.min(index
, this.rowCount
- 1));
963 this.ensureRowIsVisible(this.currentIndex
);
967 engineIconUpdated(index
) {
968 this.tree
?.invalidateCell(
970 this.tree
.columns
.getNamedColumn("engineName")
975 this.tree
?.invalidate();
978 ensureRowIsVisible(index
) {
979 this.tree
.ensureRowIsVisible(index
);
982 getSourceIndexFromDrag(dataTransfer
) {
983 return parseInt(dataTransfer
.getData(ENGINE_FLAVOR
));
986 isCheckBox(index
, column
) {
987 return column
.id
== "engineShown";
990 isEngineSelectedAndRemovable() {
991 let defaultEngine
= Services
.search
.defaultEngine
;
992 let defaultPrivateEngine
= Services
.search
.defaultPrivateEngine
;
993 // We don't allow the last remaining engine to be removed, thus the
994 // `this.lastEngineIndex != 0` check.
995 // We don't allow the default engine to be removed.
997 this.selectedIndex
!= -1 &&
998 this.lastEngineIndex
!= 0 &&
999 !this._getLocalShortcut(this.selectedIndex
) &&
1000 this.selectedEngine
.name
!= defaultEngine
.name
&&
1001 this.selectedEngine
.name
!= defaultPrivateEngine
.name
1006 * Returns the local shortcut corresponding to a tree row, or null if the row
1007 * is not a local shortcut.
1009 * @param {number} index
1010 * The tree row index.
1012 * The local shortcut object or null if the row is not a local shortcut.
1014 _getLocalShortcut(index
) {
1015 let engineCount
= this._engineStore
.engines
.length
;
1016 if (index
< engineCount
) {
1019 return UrlbarUtils
.LOCAL_SEARCH_MODES
[index
- engineCount
];
1023 * Called by UrlbarPrefs when a urlbar pref changes.
1025 * @param {string} pref
1026 * The name of the pref relative to the browser.urlbar branch.
1028 onPrefChanged(pref
) {
1029 // If one of the local shortcut prefs was toggled, toggle its row's
1031 let parts
= pref
.split(".");
1032 if (parts
[0] == "shortcuts" && parts
[1] && parts
.length
== 2) {
1037 handleEvent(aEvent
) {
1038 switch (aEvent
.type
) {
1040 if (aEvent
.target
.id
== "engineChildren") {
1041 let cell
= aEvent
.target
.parentNode
.getCellAt(
1045 if (cell
.col
?.id
== "engineKeyword") {
1046 this.#startEditingAlias(this.selectedIndex
);
1052 aEvent
.target
.id
!= "engineChildren" &&
1053 !aEvent
.target
.classList
.contains("searchEngineAction")
1055 // We don't want to toggle off selection while editing keyword
1056 // so proceed only when the input field is hidden.
1057 // We need to check that engineList.view is defined here
1058 // because the "click" event listener is on <window> and the
1059 // view might have been destroyed if the pane has been navigated
1061 if (this._engineList
.inputField
.hidden
&& this._engineList
.view
) {
1062 let selection
= this._engineList
.view
.selection
;
1063 if (selection
?.count
> 0) {
1064 selection
.toggleSelect(selection
.currentIndex
);
1066 this._engineList
.blur();
1071 switch (aEvent
.target
.id
) {
1072 case "restoreDefaultSearchEngines":
1073 this.#onRestoreDefaults();
1075 case "removeEngineButton":
1076 Services
.search
.removeEngine(this.selectedEngine
.originalEngine
);
1078 case "addEngineButton":
1080 "chrome://browser/content/preferences/dialogs/addEngine.xhtml",
1081 { features
: "resizable=no, modal=yes" }
1087 if (aEvent
.target
.id
== "engineChildren") {
1088 this.#onDragEngineStart(aEvent
);
1092 if (aEvent
.target
.id
== "engineList") {
1093 this.#onTreeKeyPress(aEvent
);
1097 if (aEvent
.target
.id
== "engineList") {
1098 this.#onTreeSelect();
1105 * Called when the restore default engines button is clicked to reset the
1106 * list of engines to their defaults.
1108 async
#onRestoreDefaults() {
1109 let num
= await
this._engineStore
.restoreDefaultEngines();
1110 this.rowCountChanged(0, num
);
1113 #onDragEngineStart(event
) {
1114 let selectedIndex
= this.selectedIndex
;
1116 // Local shortcut rows can't be dragged or re-ordered.
1117 if (this._getLocalShortcut(selectedIndex
)) {
1118 event
.preventDefault();
1122 let tree
= document
.getElementById("engineList");
1123 let cell
= tree
.getCellAt(event
.clientX
, event
.clientY
);
1124 if (selectedIndex
>= 0 && !this.isCheckBox(cell
.row
, cell
.col
)) {
1125 event
.dataTransfer
.setData(ENGINE_FLAVOR
, selectedIndex
.toString());
1126 event
.dataTransfer
.effectAllowed
= "move";
1131 document
.getElementById("removeEngineButton").disabled
=
1132 !this.isEngineSelectedAndRemovable();
1135 #onTreeKeyPress(aEvent
) {
1136 let index
= this.selectedIndex
;
1137 let tree
= document
.getElementById("engineList");
1138 if (tree
.hasAttribute("editing")) {
1142 if (aEvent
.charCode
== KeyEvent
.DOM_VK_SPACE
) {
1143 // Space toggles the checkbox.
1144 let newValue
= !this.getCellValue(
1146 tree
.columns
.getNamedColumn("engineShown")
1150 tree
.columns
.getFirstColumn(),
1153 // Prevent page from scrolling on the space key.
1154 aEvent
.preventDefault();
1156 let isMac
= Services
.appinfo
.OS
== "Darwin";
1158 (isMac
&& aEvent
.keyCode
== KeyEvent
.DOM_VK_RETURN
) ||
1159 (!isMac
&& aEvent
.keyCode
== KeyEvent
.DOM_VK_F2
)
1161 this.#startEditingAlias(index
);
1163 aEvent
.keyCode
== KeyEvent
.DOM_VK_DELETE
||
1166 aEvent
.keyCode
== KeyEvent
.DOM_VK_BACK_SPACE
&&
1167 this.isEngineSelectedAndRemovable())
1169 // Delete and Shift+Backspace (Mac) removes selected engine.
1170 Services
.search
.removeEngine(this.selectedEngine
.originalEngine
);
1176 * Triggers editing of an alias in the tree.
1178 * @param {number} index
1180 #startEditingAlias(index
) {
1181 // Local shortcut aliases can't be edited.
1182 if (this._getLocalShortcut(index
)) {
1186 let tree
= document
.getElementById("engineList");
1187 let engine
= this._engineStore
.engines
[index
];
1188 tree
.startEditing(index
, tree
.columns
.getLastColumn());
1189 tree
.inputField
.value
= engine
.alias
|| "";
1190 tree
.inputField
.select();
1195 let localModes
= UrlbarUtils
.LOCAL_SEARCH_MODES
;
1196 if (!lazy
.UrlbarPrefs
.get("scotchBonnet.enableOverride")) {
1197 localModes
= localModes
.filter(
1198 mode
=> mode
.source
!= UrlbarUtils
.RESULT_SOURCE
.ACTIONS
1201 return this._engineStore
.engines
.length
+ localModes
.length
;
1204 getImageSrc(index
, column
) {
1205 if (column
.id
== "engineName") {
1206 let shortcut
= this._getLocalShortcut(index
);
1208 return shortcut
.icon
;
1211 return this._engineStore
.engines
[index
].iconURL
;
1217 getCellText(index
, column
) {
1218 if (column
.id
== "engineName") {
1219 let shortcut
= this._getLocalShortcut(index
);
1221 return this._localShortcutL10nNames
.get(shortcut
.source
)[0] || "";
1223 return this._engineStore
.engines
[index
].name
;
1224 } else if (column
.id
== "engineKeyword") {
1225 let shortcut
= this._getLocalShortcut(index
);
1228 lazy
.UrlbarPrefs
.getScotchBonnetPref(
1229 "searchRestrictKeywords.featureGate"
1232 let keywords
= this._localShortcutL10nNames
1233 .get(shortcut
.source
)
1234 .map(keyword
=> `@${keyword.toLowerCase()}`)
1237 return `${keywords}, ${shortcut.restrict}`;
1240 return shortcut
.restrict
;
1242 return this._engineStore
.engines
[index
].originalEngine
.aliases
.join(", ");
1251 canDrop(targetIndex
, orientation
, dataTransfer
) {
1252 var sourceIndex
= this.getSourceIndexFromDrag(dataTransfer
);
1254 sourceIndex
!= -1 &&
1255 sourceIndex
!= targetIndex
&&
1256 sourceIndex
!= targetIndex
+ orientation
&&
1257 // Local shortcut rows can't be dragged or dropped on.
1258 targetIndex
< this._engineStore
.engines
.length
1262 async
drop(dropIndex
, orientation
, dataTransfer
) {
1263 // Local shortcut rows can't be dragged or dropped on. This can sometimes
1264 // be reached even though canDrop returns false for these rows.
1265 if (this._engineStore
.engines
.length
<= dropIndex
) {
1269 var sourceIndex
= this.getSourceIndexFromDrag(dataTransfer
);
1270 var sourceEngine
= this._engineStore
.engines
[sourceIndex
];
1272 const nsITreeView
= Ci
.nsITreeView
;
1273 if (dropIndex
> sourceIndex
) {
1274 if (orientation
== nsITreeView
.DROP_BEFORE
) {
1277 } else if (orientation
== nsITreeView
.DROP_AFTER
) {
1281 await
this._engineStore
.moveEngine(sourceEngine
, dropIndex
);
1282 gSearchPane
.showRestoreDefaults(true);
1284 // Redraw, and adjust selection
1286 this.selection
.select(dropIndex
);
1290 getRowProperties() {
1293 getCellProperties(index
, column
) {
1294 if (column
.id
== "engineName") {
1295 // For local shortcut rows, return the result source name so we can style
1296 // the icons in CSS.
1297 let shortcut
= this._getLocalShortcut(index
);
1299 return UrlbarUtils
.getResultSourceName(shortcut
.source
);
1304 getColumnProperties() {
1313 isContainerEmpty() {
1331 getCellValue(index
, column
) {
1332 if (column
.id
== "engineShown") {
1333 let shortcut
= this._getLocalShortcut(index
);
1335 return UrlbarPrefs
.get(shortcut
.pref
);
1337 return !this._engineStore
.engines
[index
].originalEngine
.hideOneOffButton
;
1341 toggleOpenState() {}
1343 selectionChanged() {}
1345 isEditable(index
, column
) {
1347 column
.id
!= "engineName" &&
1348 (column
.id
== "engineShown" || !this._getLocalShortcut(index
))
1351 setCellValue(index
, column
, value
) {
1352 if (column
.id
== "engineShown") {
1353 let shortcut
= this._getLocalShortcut(index
);
1355 UrlbarPrefs
.set(shortcut
.pref
, value
== "true");
1359 this._engineStore
.engines
[index
].originalEngine
.hideOneOffButton
=
1364 setCellText(index
, column
, value
) {
1365 if (column
.id
== "engineKeyword") {
1366 this.#changeKeyword(this._engineStore
.engines
[index
], value
).then(
1369 this.#startEditingAlias(index
);
1377 * Handles changing the keyword for an engine. This will check for potentially
1378 * duplicate keywords and prompt the user if necessary.
1380 * @param {object} aEngine
1381 * The engine to change.
1382 * @param {string} aNewKeyword
1384 * @returns {Promise<boolean>}
1385 * Resolves to true if the keyword was changed.
1387 async
#changeKeyword(aEngine
, aNewKeyword
) {
1388 let keyword
= aNewKeyword
.trim();
1390 let eduplicate
= false;
1393 // Check for duplicates in Places keywords.
1394 let bduplicate
= !!(await PlacesUtils
.keywords
.fetch(keyword
));
1396 // Check for duplicates in changes we haven't committed yet
1397 let engines
= this._engineStore
.engines
;
1398 let lc_keyword
= keyword
.toLocaleLowerCase();
1399 for (let engine
of engines
) {
1402 engine
.alias
.toLocaleLowerCase() == lc_keyword
&&
1403 engine
.name
!= aEngine
.name
1406 dupName
= engine
.name
;
1411 // Notify the user if they have chosen an existing engine/bookmark keyword
1412 if (eduplicate
|| bduplicate
) {
1413 let msgids
= [{ id
: "search-keyword-warning-title" }];
1416 id
: "search-keyword-warning-engine",
1417 args
: { name
: dupName
},
1420 msgids
.push({ id
: "search-keyword-warning-bookmark" });
1423 let [dtitle
, msg
] = await document
.l10n
.formatValues(msgids
);
1425 Services
.prompt
.alert(window
, dtitle
, msg
);
1430 this._engineStore
.changeEngine(aEngine
, "alias", keyword
);
1437 * Manages the default engine dropdown buttons in the search pane of preferences.
1439 class DefaultEngineDropDown
{
1443 constructor(type
, engineStore
) {
1445 this.#element
= document
.getElementById(
1446 type
== "private" ? "defaultPrivateEngine" : "defaultEngine"
1449 engineStore
.addListener(this);
1452 rowCountChanged(index
, count
, enginesList
) {
1453 // Simply rebuild the menulist, rather than trying to update the changed row.
1454 this.rebuild(enginesList
);
1457 defaultEngineChanged(type
, engine
, enginesList
) {
1458 if (type
!= this.#type
) {
1461 // If the user is going through the drop down using up/down keys, the
1462 // dropdown may still be open (eg. on Windows) when engine-default is
1463 // fired, so rebuilding the list unconditionally would get in the way.
1464 let selectedEngineName
= this.#element
.selectedItem
?.engine
?.name
;
1465 if (selectedEngineName
!= engine
.name
) {
1466 this.rebuild(enginesList
);
1470 engineIconUpdated(index
, enginesList
) {
1471 let item
= this.#element
.getItemAtIndex(index
);
1472 // Check this is the right item.
1473 if (item
?.label
== enginesList
[index
].name
) {
1474 item
.setAttribute("image", enginesList
[index
].iconURL
);
1478 async
rebuild(enginesList
) {
1480 this.#type
== "private" &&
1481 !gSearchPane
._separatePrivateDefaultPref
.value
1486 await Services
.search
[
1487 this.#type
== "normal" ? "getDefault" : "getDefaultPrivate"
1490 this.#element
.removeAllItems();
1491 for (let engine
of enginesList
) {
1492 let item
= this.#element
.appendItem(engine
.name
);
1495 "menuitem-iconic searchengine-menuitem menuitem-with-favicon"
1497 if (engine
.iconURL
) {
1498 item
.setAttribute("image", engine
.iconURL
);
1500 item
.engine
= engine
;
1501 if (engine
.name
== defaultEngine
.name
) {
1502 this.#element
.selectedItem
= item
;
1505 // This should never happen, but try and make sure we have at least one
1507 if (!this.#element
.selectedItem
) {
1508 this.#element
.selectedIndex
= 0;