Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / preferences / home.js
blob883ab422101552ccdcb6fdc1eb3225806d41775f
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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* import-globals-from extensionControlled.js */
6 /* import-globals-from preferences.js */
7 /* import-globals-from main.js */
9 // HOME PAGE
12 * Preferences:
14 * browser.startup.homepage
15 * - the user's home page, as a string; if the home page is a set of tabs,
16 * this will be those URLs separated by the pipe character "|"
17 * browser.newtabpage.enabled
18 * - determines that is shown on the user's new tab page.
19 * true = Activity Stream is shown,
20 * false = about:blank is shown
23 Preferences.addAll([
24 { id: "browser.startup.homepage", type: "wstring" },
25 { id: "pref.browser.homepage.disable_button.current_page", type: "bool" },
26 { id: "pref.browser.homepage.disable_button.bookmark_page", type: "bool" },
27 { id: "pref.browser.homepage.disable_button.restore_default", type: "bool" },
28 { id: "browser.newtabpage.enabled", type: "bool" },
29 ]);
31 const HOMEPAGE_OVERRIDE_KEY = "homepage_override";
32 const URL_OVERRIDES_TYPE = "url_overrides";
33 const NEW_TAB_KEY = "newTabURL";
35 const BLANK_HOMEPAGE_URL = "chrome://browser/content/blanktab.html";
37 var gHomePane = {
38 HOME_MODE_FIREFOX_HOME: "0",
39 HOME_MODE_BLANK: "1",
40 HOME_MODE_CUSTOM: "2",
41 HOMEPAGE_PREF: "browser.startup.homepage",
42 NEWTAB_ENABLED_PREF: "browser.newtabpage.enabled",
43 ACTIVITY_STREAM_PREF_BRANCH: "browser.newtabpage.activity-stream.",
45 get homePanePrefs() {
46 return Preferences.getAll().filter(pref =>
47 pref.id.includes(this.ACTIVITY_STREAM_PREF_BRANCH)
51 get isPocketNewtabEnabled() {
52 const value = Services.prefs.getStringPref(
53 "browser.newtabpage.activity-stream.discoverystream.config",
56 if (value) {
57 try {
58 return JSON.parse(value).enabled;
59 } catch (e) {
60 console.error("Failed to parse Discovery Stream pref.");
64 return false;
67 async syncToNewTabPref() {
68 let menulist = document.getElementById("newTabMode");
70 if (["0", "1"].includes(menulist.value)) {
71 let newtabEnabledPref = Services.prefs.getBoolPref(
72 this.NEWTAB_ENABLED_PREF,
73 true
75 let newValue = menulist.value !== this.HOME_MODE_BLANK;
76 // Only set this if the pref has changed, otherwise the pref change will trigger other listeners to repeat.
77 if (newtabEnabledPref !== newValue) {
78 Services.prefs.setBoolPref(this.NEWTAB_ENABLED_PREF, newValue);
80 let selectedAddon = ExtensionSettingsStore.getSetting(
81 URL_OVERRIDES_TYPE,
82 NEW_TAB_KEY
84 if (selectedAddon) {
85 ExtensionSettingsStore.select(null, URL_OVERRIDES_TYPE, NEW_TAB_KEY);
87 } else {
88 let addon = await AddonManager.getAddonByID(menulist.value);
89 if (addon && addon.isActive) {
90 ExtensionSettingsStore.select(
91 addon.id,
92 URL_OVERRIDES_TYPE,
93 NEW_TAB_KEY
99 async syncFromNewTabPref() {
100 let menulist = document.getElementById("newTabMode");
102 // If the new tab url was changed to about:blank or about:newtab
103 if (
104 AboutNewTab.newTabURL === "about:newtab" ||
105 AboutNewTab.newTabURL === "about:blank" ||
106 AboutNewTab.newTabURL === BLANK_HOMEPAGE_URL
108 let newtabEnabledPref = Services.prefs.getBoolPref(
109 this.NEWTAB_ENABLED_PREF,
110 true
112 let newValue = newtabEnabledPref
113 ? this.HOME_MODE_FIREFOX_HOME
114 : this.HOME_MODE_BLANK;
115 if (newValue !== menulist.value) {
116 menulist.value = newValue;
118 menulist.disabled = Preferences.get(this.NEWTAB_ENABLED_PREF).locked;
119 // If change was triggered by installing an addon we need to update
120 // the value of the menulist to be that addon.
121 } else {
122 let selectedAddon = ExtensionSettingsStore.getSetting(
123 URL_OVERRIDES_TYPE,
124 NEW_TAB_KEY
126 if (selectedAddon && menulist.value !== selectedAddon.id) {
127 menulist.value = selectedAddon.id;
133 * _updateMenuInterface: adds items to or removes them from the menulists
134 * @param {string} selectId Optional Id of the menulist to add or remove items from.
135 * If not included this will update both home and newtab menus.
137 async _updateMenuInterface(selectId) {
138 let selects;
139 if (selectId) {
140 selects = [document.getElementById(selectId)];
141 } else {
142 let newTabSelect = document.getElementById("newTabMode");
143 let homeSelect = document.getElementById("homeMode");
144 selects = [homeSelect, newTabSelect];
147 for (let select of selects) {
148 // Remove addons from the menu popup which are no longer installed, or disabled.
149 // let menuOptions = select.menupopup.childNodes;
150 let menuOptions = Array.from(select.menupopup.childNodes);
152 for (let option of menuOptions) {
153 // If the value is not a number, assume it is an addon ID
154 if (!/^\d+$/.test(option.value)) {
155 let addon = await AddonManager.getAddonByID(option.value);
156 if (option && (!addon || !addon.isActive)) {
157 option.remove();
162 let extensionOptions;
163 await ExtensionSettingsStore.initialize();
164 if (select.id === "homeMode") {
165 extensionOptions = ExtensionSettingsStore.getAllSettings(
166 PREF_SETTING_TYPE,
167 HOMEPAGE_OVERRIDE_KEY
169 } else {
170 extensionOptions = ExtensionSettingsStore.getAllSettings(
171 URL_OVERRIDES_TYPE,
172 NEW_TAB_KEY
175 let addons = await AddonManager.getAddonsByIDs(
176 extensionOptions.map(a => a.id)
179 // Add addon options to the menu popups
180 let menupopup = select.querySelector("menupopup");
181 for (let addon of addons) {
182 if (!addon || !addon.id || !addon.isActive) {
183 continue;
185 let currentOption = select.querySelector(
186 `[value="${CSS.escape(addon.id)}"]`
188 if (!currentOption) {
189 let option = document.createXULElement("menuitem");
190 option.classList.add("addon-with-favicon");
191 option.value = addon.id;
192 option.label = addon.name;
193 menupopup.append(option);
194 option.querySelector("image").src = addon.iconURL;
196 let setting = extensionOptions.find(o => o.id == addon.id);
197 if (
198 (select.id === "homeMode" && setting.value == HomePage.get()) ||
199 (select.id === "newTabMode" && setting.value == AboutNewTab.newTabURL)
201 select.value = addon.id;
208 * watchNewTab: Listen for changes to the new tab url and enable/disable appropriate
209 * areas of the UI.
211 watchNewTab() {
212 let newTabObserver = () => {
213 this.syncFromNewTabPref();
214 this._updateMenuInterface("newTabMode");
216 Services.obs.addObserver(newTabObserver, "newtab-url-changed");
217 window.addEventListener("unload", () => {
218 Services.obs.removeObserver(newTabObserver, "newtab-url-changed");
223 * watchHomePrefChange: Listen for preferences changes on the Home Tab in order to
224 * show the appropriate home menu selection.
226 watchHomePrefChange() {
227 const homePrefObserver = (subject, topic, data) => {
228 // only update this UI if it is exactly the HOMEPAGE_PREF, not other prefs with the same root.
229 if (data && data != this.HOMEPAGE_PREF) {
230 return;
232 this._updateUseCurrentButton();
233 this._renderCustomSettings();
234 this._handleHomePageOverrides();
235 this._updateMenuInterface("homeMode");
238 Services.prefs.addObserver(this.HOMEPAGE_PREF, homePrefObserver);
239 window.addEventListener("unload", () => {
240 Services.prefs.removeObserver(this.HOMEPAGE_PREF, homePrefObserver);
245 * Listen extension changes on the New Tab and Home Tab
246 * in order to update the UI and show or hide the Restore Defaults button.
248 watchExtensionPrefChange() {
249 const extensionSettingChanged = (evt, setting) => {
250 if (setting.key == "homepage_override" && setting.type == "prefs") {
251 this._updateMenuInterface("homeMode");
252 } else if (
253 setting.key == "newTabURL" &&
254 setting.type == "url_overrides"
256 this._updateMenuInterface("newTabMode");
260 Management.on("extension-setting-changed", extensionSettingChanged);
261 window.addEventListener("unload", () => {
262 Management.off("extension-setting-changed", extensionSettingChanged);
267 * Listen for all preferences changes on the Home Tab in order to show or
268 * hide the Restore Defaults button.
270 watchHomeTabPrefChange() {
271 const observer = () => this.toggleRestoreDefaultsBtn();
272 Services.prefs.addObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer);
273 Services.prefs.addObserver(this.HOMEPAGE_PREF, observer);
274 Services.prefs.addObserver(this.NEWTAB_ENABLED_PREF, observer);
276 window.addEventListener("unload", () => {
277 Services.prefs.removeObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer);
278 Services.prefs.removeObserver(this.HOMEPAGE_PREF, observer);
279 Services.prefs.removeObserver(this.NEWTAB_ENABLED_PREF, observer);
284 * _renderCustomSettings: Hides or shows the UI for setting a custom
285 * homepage URL
286 * @param {obj} options
287 * @param {bool} options.shouldShow Should the custom UI be shown?
288 * @param {bool} options.isControlled Is an extension controlling the home page?
290 _renderCustomSettings(options = {}) {
291 let { shouldShow, isControlled } = options;
292 const customSettingsContainerEl = document.getElementById("customSettings");
293 const customUrlEl = document.getElementById("homePageUrl");
294 const homePage = HomePage.get();
295 const isHomePageCustom =
296 (!this._isHomePageDefaultValue() &&
297 !this.isHomePageBlank() &&
298 !isControlled) ||
299 homePage.locked;
301 if (typeof shouldShow === "undefined") {
302 shouldShow = isHomePageCustom;
304 customSettingsContainerEl.hidden = !shouldShow;
306 // We can't use isHomePageDefaultValue and isHomePageBlank here because we want to disregard the blank
307 // possibility triggered by the browser.startup.page being 0.
308 // We also skip when HomePage is locked because it might be locked to a default that isn't "about:home"
309 // (and it makes existing tests happy).
310 let newValue;
311 if (
312 this._isBlankPage(homePage) ||
313 (HomePage.isDefault && !HomePage.locked)
315 newValue = "";
316 } else {
317 newValue = homePage;
319 if (customUrlEl.value !== newValue) {
320 customUrlEl.value = newValue;
325 * _isHomePageDefaultValue
326 * @returns {bool} Is the homepage set to the default pref value?
328 _isHomePageDefaultValue() {
329 const startupPref = Preferences.get("browser.startup.page");
330 return (
331 startupPref.value !== gMainPane.STARTUP_PREF_BLANK && HomePage.isDefault
336 * isHomePageBlank
337 * @returns {bool} Is the homepage set to about:blank?
339 isHomePageBlank() {
340 const startupPref = Preferences.get("browser.startup.page");
341 return (
342 ["about:blank", BLANK_HOMEPAGE_URL, ""].includes(HomePage.get()) ||
343 startupPref.value === gMainPane.STARTUP_PREF_BLANK
348 * _isTabAboutPreferencesOrSettings: Is a given tab set to about:preferences or about:settings?
349 * @param {Element} aTab A tab element
350 * @returns {bool} Is the linkedBrowser of aElement set to about:preferences or about:settings?
352 _isTabAboutPreferencesOrSettings(aTab) {
353 return (
354 aTab.linkedBrowser.currentURI.spec.startsWith("about:preferences") ||
355 aTab.linkedBrowser.currentURI.spec.startsWith("about:settings")
360 * _getTabsForHomePage
361 * @returns {Array} An array of current tabs
363 _getTabsForHomePage() {
364 let tabs = [];
365 let win = Services.wm.getMostRecentWindow("navigator:browser");
367 // We should only include visible & non-pinned tabs
368 if (
369 win &&
370 win.document.documentElement.getAttribute("windowtype") ===
371 "navigator:browser"
373 tabs = win.gBrowser.visibleTabs.slice(win.gBrowser.pinnedTabCount);
374 tabs = tabs.filter(tab => !this._isTabAboutPreferencesOrSettings(tab));
375 // XXX: Bug 1441637 - Fix tabbrowser to report tab.closing before it blurs it
376 tabs = tabs.filter(tab => !tab.closing);
379 return tabs;
382 _renderHomepageMode(controllingExtension) {
383 const isDefault = this._isHomePageDefaultValue();
384 const isBlank = this.isHomePageBlank();
385 const el = document.getElementById("homeMode");
386 let newValue;
388 if (controllingExtension && controllingExtension.id) {
389 newValue = controllingExtension.id;
390 } else if (isDefault) {
391 newValue = this.HOME_MODE_FIREFOX_HOME;
392 } else if (isBlank) {
393 newValue = this.HOME_MODE_BLANK;
394 } else {
395 newValue = this.HOME_MODE_CUSTOM;
397 if (el.value !== newValue) {
398 el.value = newValue;
402 _setInputDisabledStates(isControlled) {
403 let tabCount = this._getTabsForHomePage().length;
405 // Disable or enable the inputs based on if this is controlled by an extension.
406 document
407 .querySelectorAll(".check-home-page-controlled")
408 .forEach(element => {
409 let isDisabled;
410 let pref =
411 element.getAttribute("preference") ||
412 element.getAttribute("data-preference-related");
413 if (!pref) {
414 throw new Error(
415 `Element with id ${element.id} did not have preference or data-preference-related attribute defined.`
419 if (pref === this.HOMEPAGE_PREF) {
420 isDisabled = HomePage.locked;
421 } else {
422 isDisabled = Preferences.get(pref).locked || isControlled;
425 if (pref === "pref.browser.disable_button.current_page") {
426 // Special case for current_page to disable it if tabCount is 0
427 isDisabled = isDisabled || tabCount < 1;
430 element.disabled = isDisabled;
434 async _handleHomePageOverrides() {
435 let controllingExtension;
436 if (HomePage.locked) {
437 // Disable inputs if they are locked.
438 this._renderCustomSettings();
439 this._setInputDisabledStates(false);
440 } else {
441 if (HomePage.get().startsWith("moz-extension:")) {
442 controllingExtension = await getControllingExtension(
443 PREF_SETTING_TYPE,
444 HOMEPAGE_OVERRIDE_KEY
447 this._setInputDisabledStates();
448 this._renderCustomSettings({
449 isControlled: !!controllingExtension,
452 this._renderHomepageMode(controllingExtension);
455 onMenuChange(event) {
456 const { value } = event.target;
457 const startupPref = Preferences.get("browser.startup.page");
458 let selectedAddon = ExtensionSettingsStore.getSetting(
459 PREF_SETTING_TYPE,
460 HOMEPAGE_OVERRIDE_KEY
463 switch (value) {
464 case this.HOME_MODE_FIREFOX_HOME:
465 if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
466 startupPref.value = gMainPane.STARTUP_PREF_HOMEPAGE;
468 if (!HomePage.isDefault) {
469 HomePage.reset();
470 } else {
471 this._renderCustomSettings({ shouldShow: false });
473 if (selectedAddon) {
474 ExtensionSettingsStore.select(
475 null,
476 PREF_SETTING_TYPE,
477 HOMEPAGE_OVERRIDE_KEY
480 break;
481 case this.HOME_MODE_BLANK:
482 if (!this._isBlankPage(HomePage.get())) {
483 HomePage.safeSet(BLANK_HOMEPAGE_URL);
484 } else {
485 this._renderCustomSettings({ shouldShow: false });
487 if (selectedAddon) {
488 ExtensionSettingsStore.select(
489 null,
490 PREF_SETTING_TYPE,
491 HOMEPAGE_OVERRIDE_KEY
494 break;
495 case this.HOME_MODE_CUSTOM:
496 if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
497 Services.prefs.clearUserPref(startupPref.id);
499 if (HomePage.getDefault() != HomePage.getOriginalDefault()) {
500 HomePage.clear();
502 this._renderCustomSettings({ shouldShow: true });
503 if (selectedAddon) {
504 ExtensionSettingsStore.select(
505 null,
506 PREF_SETTING_TYPE,
507 HOMEPAGE_OVERRIDE_KEY
510 break;
511 // extensions will have a variety of values as their ID, so treat it as default
512 default:
513 AddonManager.getAddonByID(value).then(addon => {
514 if (addon && addon.isActive) {
515 ExtensionPreferencesManager.selectSetting(
516 addon.id,
517 HOMEPAGE_OVERRIDE_KEY
520 this._renderCustomSettings({ shouldShow: false });
526 * Switches the "Use Current Page" button between its singular and plural
527 * forms.
529 async _updateUseCurrentButton() {
530 let useCurrent = document.getElementById("useCurrentBtn");
531 let tabs = this._getTabsForHomePage();
532 const tabCount = tabs.length;
533 document.l10n.setAttributes(useCurrent, "use-current-pages", { tabCount });
535 // If the homepage is controlled by an extension then you can't use this.
536 if (
537 await getControllingExtensionInfo(
538 PREF_SETTING_TYPE,
539 HOMEPAGE_OVERRIDE_KEY
542 return;
545 // In this case, the button's disabled state is set by preferences.xml.
546 let prefName = "pref.browser.homepage.disable_button.current_page";
547 if (Preferences.get(prefName).locked) {
548 return;
551 useCurrent.disabled = tabCount < 1;
555 * Sets the home page to the URL(s) of any currently opened tab(s),
556 * updating about:preferences#home UI to reflect this.
558 setHomePageToCurrent() {
559 let tabs = this._getTabsForHomePage();
560 function getTabURI(t) {
561 return t.linkedBrowser.currentURI.spec;
564 // FIXME Bug 244192: using dangerous "|" joiner!
565 if (tabs.length) {
566 HomePage.set(tabs.map(getTabURI).join("|")).catch(console.error);
570 _setHomePageToBookmarkClosed(rv, aEvent) {
571 if (aEvent.detail.button != "accept") {
572 return;
574 if (rv.urls && rv.names) {
575 // XXX still using dangerous "|" joiner!
576 HomePage.set(rv.urls.join("|")).catch(console.error);
581 * Displays a dialog in which the user can select a bookmark to use as home
582 * page. If the user selects a bookmark, that bookmark's name is displayed in
583 * UI and the bookmark's address is stored to the home page preference.
585 setHomePageToBookmark() {
586 const rv = { urls: null, names: null };
587 gSubDialog.open(
588 "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml",
590 features: "resizable=yes, modal=yes",
591 closingCallback: this._setHomePageToBookmarkClosed.bind(this, rv),
597 restoreDefaultHomePage() {
598 HomePage.reset();
599 this._handleHomePageOverrides();
600 Services.prefs.clearUserPref(this.NEWTAB_ENABLED_PREF);
601 AboutNewTab.resetNewTabURL();
604 onCustomHomePageChange(event) {
605 const value = event.target.value || HomePage.getDefault();
606 HomePage.set(value).catch(console.error);
610 * Check all Home Tab preferences for user set values.
612 _changedHomeTabDefaultPrefs() {
613 // If Discovery Stream is enabled Firefox Home Content preference options are hidden
614 const homeContentChanged =
615 !this.isPocketNewtabEnabled &&
616 this.homePanePrefs.some(pref => pref.hasUserValue);
617 const newtabPref = Preferences.get(this.NEWTAB_ENABLED_PREF);
618 const extensionControlled = Preferences.get(
619 "browser.startup.homepage_override.extensionControlled"
622 return (
623 homeContentChanged ||
624 HomePage.overridden ||
625 newtabPref.hasUserValue ||
626 AboutNewTab.newTabURLOverridden ||
627 extensionControlled
631 _isBlankPage(url) {
632 return url == "about:blank" || url == BLANK_HOMEPAGE_URL;
636 * Show the Restore Defaults button if any preference on the Home tab was
637 * changed, or hide it otherwise.
639 toggleRestoreDefaultsBtn() {
640 const btn = document.getElementById("restoreDefaultHomePageBtn");
641 const prefChanged = this._changedHomeTabDefaultPrefs();
642 if (prefChanged) {
643 btn.style.removeProperty("visibility");
644 } else {
645 btn.style.visibility = "hidden";
650 * Set all prefs on the Home tab back to their default values.
652 restoreDefaultPrefsForHome() {
653 this.restoreDefaultHomePage();
654 // If Discovery Stream is enabled Firefox Home Content preference options are hidden
655 if (!this.isPocketNewtabEnabled) {
656 this.homePanePrefs.forEach(pref => Services.prefs.clearUserPref(pref.id));
660 init() {
661 // Event Listeners
662 document
663 .getElementById("homePageUrl")
664 .addEventListener("change", this.onCustomHomePageChange.bind(this));
665 document
666 .getElementById("useCurrentBtn")
667 .addEventListener("command", this.setHomePageToCurrent.bind(this));
668 document
669 .getElementById("useBookmarkBtn")
670 .addEventListener("command", this.setHomePageToBookmark.bind(this));
671 document
672 .getElementById("restoreDefaultHomePageBtn")
673 .addEventListener("command", this.restoreDefaultPrefsForHome.bind(this));
675 // Setup the add-on options for the new tab section before registering the
676 // listener.
677 this._updateMenuInterface();
678 document
679 .getElementById("newTabMode")
680 .addEventListener("command", this.syncToNewTabPref.bind(this));
681 document
682 .getElementById("homeMode")
683 .addEventListener("command", this.onMenuChange.bind(this));
685 this._updateUseCurrentButton();
686 this._handleHomePageOverrides();
687 this.syncFromNewTabPref();
688 window.addEventListener("focus", this._updateUseCurrentButton.bind(this));
690 // Extension/override-related events
691 this.watchNewTab();
692 this.watchHomePrefChange();
693 this.watchExtensionPrefChange();
694 this.watchHomeTabPrefChange();
695 // Notify observers that the UI is now ready
696 Services.obs.notifyObservers(window, "home-pane-loaded");