diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index ea6155a1d854..e7bfaf43d1d3 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -386,6 +386,10 @@ pref("browser.search.hiddenOneOffs", "");
// Mirrors whether the search-container widget is in the navigation toolbar.
pref("browser.search.widget.inNavBar", false);
+// Enables display of the options for the user using a separate default search
+// engine in private browsing mode.
+pref("browser.search.separatePrivateDefault.ui.enabled", false);
+
pref("browser.sessionhistory.max_entries", 50);
// Built-in default permissions.
diff --git a/browser/components/preferences/in-content/privacy.inc.xul b/browser/components/preferences/in-content/privacy.inc.xul
index f2d59cae1efa..6c3cf966df74 100644
--- a/browser/components/preferences/in-content/privacy.inc.xul
+++ b/browser/components/preferences/in-content/privacy.inc.xul
@@ -621,7 +621,8 @@
+ hidden="true"
+ data-subcategory="locationBar">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/preferences/in-content/search.js b/browser/components/preferences/in-content/search.js
index aecbf7ddb681..748d16d8b344 100644
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -22,6 +22,8 @@ Preferences.addAll([
{ id: "browser.search.hiddenOneOffs", type: "unichar" },
{ id: "browser.search.widget.inNavBar", type: "bool" },
{ id: "browser.urlbar.matchBuckets", type: "string" },
+ { id: "browser.search.separatePrivateDefault", type: "bool" },
+ { id: "browser.search.separatePrivateDefault.ui.enabled", type: "bool" },
]);
const ENGINE_FLAVOR = "text/x-moz-search-engine";
@@ -43,7 +45,7 @@ var gSearchPane = {
init() {
gEngineView = new EngineView(new EngineStore());
document.getElementById("engineList").view = gEngineView;
- this.buildDefaultEngineDropDown();
+ this.buildDefaultEngineDropDowns().catch(console.error);
if (
Services.policies &&
@@ -84,10 +86,67 @@ var gSearchPane = {
urlbarSuggestsPref.value = urlbarSuggests.checked;
});
+ setEventListener(
+ "browserSeparateDefaultEngine",
+ "command",
+ this._onBrowserSeparateDefaultEngineChange.bind(this)
+ );
+ setEventListener("openLocationBarPrivacyPreferences", "click", function(
+ event
+ ) {
+ if (event.button == 0) {
+ gotoPref("privacy-locationBar");
+ }
+ });
+
+ this._initDefaultEngines();
this._initShowSearchSuggestionsFirst();
this._updateSuggestionCheckboxes();
},
+ /**
+ * 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);
+ },
+
+ _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;
+ },
+
_initShowSearchSuggestionsFirst() {
this._urlbarSuggestionsPosPref = Preferences.get(
"browser.urlbar.matchBuckets"
@@ -163,12 +222,37 @@ var gSearchPane = {
permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB;
},
- async buildDefaultEngineDropDown() {
- // This is called each time something affects the list of engines.
- let list = document.getElementById("defaultEngine");
- // Set selection to the current default engine.
- let currentEngine = (await Services.search.getDefault()).name;
+ /**
+ * 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) {
@@ -195,6 +279,12 @@ var gSearchPane = {
}
});
+ // We don't currently support overriding the engine for private mode with
+ // extensions.
+ if (isPrivate) {
+ return;
+ }
+
handleControllingExtension(SEARCH_TYPE, SEARCH_KEY);
let searchEngineListener = {
observe(subject, topic, data) {
@@ -241,10 +331,15 @@ var gSearchPane = {
case "":
if (
aEvent.target.parentNode &&
- aEvent.target.parentNode.parentNode &&
- aEvent.target.parentNode.parentNode.id == "defaultEngine"
+ aEvent.target.parentNode.parentNode
) {
- gSearchPane.setDefaultEngine();
+ if (aEvent.target.parentNode.parentNode.id == "defaultEngine") {
+ gSearchPane.setDefaultEngine();
+ } else if (
+ aEvent.target.parentNode.parentNode.id == "defaultPrivateEngine"
+ ) {
+ gSearchPane.setDefaultPrivateEngine();
+ }
}
break;
case "restoreDefaultSearchEngines":
@@ -291,7 +386,7 @@ var gSearchPane = {
case "engine-added":
gEngineView._engineStore.addEngine(aEngine);
gEngineView.rowCountChanged(gEngineView.lastIndex, 1);
- gSearchPane.buildDefaultEngineDropDown();
+ gSearchPane.buildDefaultEngineDropDowns();
break;
case "engine-changed":
gEngineView._engineStore.reloadIcons();
@@ -300,16 +395,28 @@ var gSearchPane = {
case "engine-removed":
gSearchPane.remove(aEngine);
break;
- case "engine-default":
+ 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 != aEngine.name) {
- gSearchPane.buildDefaultEngineDropDown();
+ gSearchPane.buildDefaultEngineDropDowns();
}
break;
+ }
+ case "engine-default-private": {
+ // 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 != aEngine.name) {
+ gSearchPane.buildDefaultEngineDropDowns();
+ }
+ break;
+ }
}
}
},
@@ -453,6 +560,12 @@ var gSearchPane = {
);
ExtensionSettingsStore.setByUser(SEARCH_TYPE, SEARCH_KEY);
},
+
+ async setDefaultPrivateEngine() {
+ await Services.search.setDefaultPrivate(
+ document.getElementById("defaultPrivateEngine").selectedItem.engine
+ );
+ },
};
function onDragEngineStart(event) {
@@ -480,7 +593,7 @@ function EngineStore() {
gEngineView.rowCountChanged(gEngineView.lastIndex, 1);
}
this._defaultEngines = defaultEngines.map(this._cloneEngine, this);
- gSearchPane.buildDefaultEngineDropDown();
+ gSearchPane.buildDefaultEngineDropDowns();
// check if we need to disable the restore defaults button
var someHidden = this._defaultEngines.some(e => e.hidden);
@@ -563,7 +676,7 @@ EngineStore.prototype = {
if (this._defaultEngines.some(this._isSameEngine, removedEngine)) {
gSearchPane.showRestoreDefaults(true);
}
- gSearchPane.buildDefaultEngineDropDown();
+ gSearchPane.buildDefaultEngineDropDowns();
return index;
},
@@ -592,7 +705,7 @@ EngineStore.prototype = {
}
Services.search.resetToOriginalDefaultEngine();
gSearchPane.showRestoreDefaults(false);
- gSearchPane.buildDefaultEngineDropDown();
+ gSearchPane.buildDefaultEngineDropDowns();
return added;
},
@@ -720,7 +833,7 @@ EngineView.prototype = {
await this._engineStore.moveEngine(sourceEngine, dropIndex);
gSearchPane.showRestoreDefaults(true);
- gSearchPane.buildDefaultEngineDropDown();
+ gSearchPane.buildDefaultEngineDropDowns();
// Redraw, and adjust selection
this.invalidate();
diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini
index e3f4562f5cb9..f705f62ca313 100644
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -81,6 +81,10 @@ skip-if = e10s
[browser_privacypane_3.js]
[browser_privacy_passwordGenerationAndAutofill.js]
[browser_sanitizeOnShutdown_prefLocked.js]
+[browser_searchDefaultEngine.js]
+support-files =
+ engine1/manifest.json
+ engine2/manifest.json
[browser_searchShowSuggestionsFirst.js]
[browser_searchsuggestions.js]
[browser_security-1.js]
diff --git a/browser/components/preferences/in-content/tests/browser_searchDefaultEngine.js b/browser/components/preferences/in-content/tests/browser_searchDefaultEngine.js
new file mode 100644
index 000000000000..45834c80b7d2
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_searchDefaultEngine.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { SearchTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+);
+
+add_task(async function setup() {
+ const engine1 = await Services.search.addEngineWithDetails("engine1", {
+ method: "get",
+ template: "http://example.com/engine1?search={searchTerms}",
+ });
+
+ const engine2 = await Services.search.addEngineWithDetails("engine2", {
+ method: "get",
+ template: "http://example.com/engine2?search={searchTerms}",
+ });
+
+ const originalDefault = await Services.search.getDefault();
+ const originalDefaultPrivate = await Services.search.getDefaultPrivate();
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(originalDefault);
+ await Services.search.setDefaultPrivate(originalDefaultPrivate);
+
+ await Services.search.removeEngine(engine1);
+ await Services.search.removeEngine(engine2);
+ });
+});
+
+add_task(async function test_openWithPrivateDefaultNotEnabledFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", false],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const separateEngineCheckbox = doc.getElementById(
+ "browserSeparateDefaultEngine"
+ );
+ const privateDefaultVbox = doc.getElementById(
+ "browserPrivateEngineSelection"
+ );
+
+ Assert.ok(
+ separateEngineCheckbox.hidden,
+ "Should have hidden the separate search engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should have hidden the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", true]],
+ });
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should have displayed the separate search engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should not have displayed the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should still be displaying the separate search engine checkbox"
+ );
+ Assert.ok(
+ !privateDefaultVbox.hidden,
+ "Should have displayed the private engine selection box"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_openWithPrivateDefaultEnabledFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const separateEngineCheckbox = doc.getElementById(
+ "browserSeparateDefaultEngine"
+ );
+ const privateDefaultVbox = doc.getElementById(
+ "browserPrivateEngineSelection"
+ );
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should not have hidden the separate search engine checkbox"
+ );
+ Assert.ok(
+ !privateDefaultVbox.hidden,
+ "Should not have hidden the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", false]],
+ });
+
+ Assert.ok(
+ !separateEngineCheckbox.hidden,
+ "Should not have hidden the separate search engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should have hidden the private engine selection box"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault.ui.enabled", false]],
+ });
+
+ Assert.ok(
+ separateEngineCheckbox.hidden,
+ "Should have hidden the separate private engine checkbox"
+ );
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should still be hiding the private engine selection box"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_separatePrivateDefault() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const separateEngineCheckbox = doc.getElementById(
+ "browserSeparateDefaultEngine"
+ );
+ const privateDefaultVbox = doc.getElementById(
+ "browserPrivateEngineSelection"
+ );
+
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should not be displaying the private engine selection box"
+ );
+
+ separateEngineCheckbox.checked = false;
+ separateEngineCheckbox.doCommand();
+
+ Assert.ok(
+ Services.prefs.getBoolPref("browser.search.separatePrivateDefault"),
+ "Should have correctly set the pref"
+ );
+
+ Assert.ok(
+ !privateDefaultVbox.hidden,
+ "Should be displaying the private engine selection box"
+ );
+
+ separateEngineCheckbox.checked = true;
+ separateEngineCheckbox.doCommand();
+
+ Assert.ok(
+ !Services.prefs.getBoolPref("browser.search.separatePrivateDefault"),
+ "Should have correctly turned the pref off"
+ );
+
+ Assert.ok(
+ privateDefaultVbox.hidden,
+ "Should have hidden the private engine selection box"
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+async function setDefaultEngine(
+ testPrivate,
+ currentEngineName,
+ expectedEngineName
+) {
+ await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
+
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const defaultEngineSelector = doc.getElementById(
+ testPrivate ? "defaultPrivateEngine" : "defaultEngine"
+ );
+
+ Assert.equal(
+ defaultEngineSelector.selectedItem.engine.name,
+ currentEngineName,
+ "Should have the correct engine as default on first open"
+ );
+
+ const popup = defaultEngineSelector.menupopup;
+ const popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ defaultEngineSelector,
+ {},
+ gBrowser.selectedBrowser
+ );
+ await popupShown;
+
+ const items = Array.from(popup.children);
+ const engine2Item = items.find(
+ item => item.engine.name == expectedEngineName
+ );
+
+ const defaultChanged = SearchTestUtils.promiseSearchNotification(
+ testPrivate ? "engine-default-private" : "engine-default",
+ "browser-search-engine-modified"
+ );
+ const popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ engine2Item,
+ {},
+ gBrowser.selectedBrowser
+ );
+ await popupHidden;
+ await defaultChanged;
+
+ const newDefault = testPrivate
+ ? await Services.search.getDefaultPrivate()
+ : await Services.search.getDefault();
+ Assert.equal(
+ newDefault.name,
+ expectedEngineName,
+ "Should have changed the default engine to engine2"
+ );
+}
+
+add_task(async function test_setDefaultEngine() {
+ const engine1 = Services.search.getEngineByName("engine1");
+
+ // Set an initial default so we have a known engine.
+ await Services.search.setDefault(engine1);
+
+ await setDefaultEngine(false, "engine1", "engine2");
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_setPrivateDefaultEngine() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ],
+ });
+
+ const engine2 = Services.search.getEngineByName("engine2");
+
+ // Set an initial default so we have a known engine.
+ await Services.search.setDefaultPrivate(engine2);
+
+ await setDefaultEngine(true, "engine2", "engine1");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/preferences/in-content/tests/engine1/manifest.json b/browser/components/preferences/in-content/tests/engine1/manifest.json
new file mode 100644
index 000000000000..e30a128de118
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/engine1/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "engine1",
+ "manifest_version": 2,
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "engine1@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine1",
+ "search_url": "https://1.example.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/preferences/in-content/tests/engine2/manifest.json b/browser/components/preferences/in-content/tests/engine2/manifest.json
new file mode 100644
index 000000000000..c69472ffcb50
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/engine2/manifest.json
@@ -0,0 +1,27 @@
+{
+ "name": "engine2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "engine2@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "description": "A small test engine",
+ "icons": {
+ "16": "favicon.ico"
+ },
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "engine2",
+ "search_url": "https://2.example.com/search",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl
index 5bf3e366773d..c8587d56f7d8 100644
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -629,6 +629,13 @@ search-bar-shown =
search-engine-default-header = Default Search Engine
search-engine-default-desc = Choose the default search engine to use in the address bar and search bar.
+search-engine-default-private-desc = Choose the default search engine to use in Private Windows.
+search-separate-default-engine =
+ .label = Use this search engine in Private Windows
+ .accesskey = U
+
+search-suggestions-header = Search Suggestions
+search-suggestions-desc = Choose how suggestions from search engines appear.
search-suggestions-option =
.label = Provide search suggestions
@@ -646,6 +653,8 @@ search-show-suggestions-url-bar-option =
search-show-suggestions-above-history-option =
.label = Show search suggestions ahead of browsing history in address bar results
+suggestions-addressbar-settings = Change preferences for browsing history, bookmarks, and tab suggestions
+
search-suggestions-cant-show = Search suggestions will not be shown in location bar results because you have configured { -brand-short-name } to never remember history.
search-one-click-header = One-Click Search Engines
diff --git a/browser/themes/shared/incontentprefs/preferences.inc.css b/browser/themes/shared/incontentprefs/preferences.inc.css
index d93321143571..98d21153f6db 100644
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -54,7 +54,7 @@ html|h2 {
}
description.indent,
-.indent > description {
+.indent:not(#browserPrivateEngineSelection) > description {
color: var(--in-content-deemphasized-text);
}
diff --git a/browser/themes/shared/incontentprefs/search.css b/browser/themes/shared/incontentprefs/search.css
index 65d70fc7c285..89d5bdce6add 100644
--- a/browser/themes/shared/incontentprefs/search.css
+++ b/browser/themes/shared/incontentprefs/search.css
@@ -28,11 +28,13 @@
transform: scaleX(-1);
}
-#defaultEngine {
+#defaultEngine,
+#defaultPrivateEngine {
margin-inline-start: 0;
}
-#defaultEngine > .menulist-label-box > .menulist-icon {
+#defaultEngine > .menulist-label-box > .menulist-icon,
+#defaultPrivateEngine > .menulist-label-box > .menulist-icon {
height: 16px;
}