summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences/main.js')
-rw-r--r--browser/components/preferences/main.js4258
1 files changed, 4258 insertions, 0 deletions
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
new file mode 100644
index 0000000000..3f5db88709
--- /dev/null
+++ b/browser/components/preferences/main.js
@@ -0,0 +1,4258 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from extensionControlled.js */
+/* import-globals-from preferences.js */
+/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
+/* import-globals-from /browser/base/content/aboutDialog-appUpdater.js */
+/* global MozXULElement */
+
+ChromeUtils.defineESModuleGetters(this, {
+ BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.sys.mjs",
+ MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
+});
+
+// Constants & Enumeration Values
+const TYPE_PDF = "application/pdf";
+
+const PREF_PDFJS_DISABLED = "pdfjs.disabled";
+
+// Pref for when containers is being controlled
+const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension";
+
+// Strings to identify ExtensionSettingsStore overrides
+const CONTAINERS_KEY = "privacy.containers";
+
+const PREF_USE_SYSTEM_COLORS = "browser.display.use_system_colors";
+const PREF_CONTENT_APPEARANCE =
+ "layout.css.prefers-color-scheme.content-override";
+const FORCED_COLORS_QUERY = matchMedia("(forced-colors)");
+
+const AUTO_UPDATE_CHANGED_TOPIC =
+ UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic;
+const BACKGROUND_UPDATE_CHANGED_TOPIC =
+ UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
+ .observerTopic;
+
+const ICON_URL_APP =
+ AppConstants.platform == "linux"
+ ? "moz-icon://dummy.exe?size=16"
+ : "chrome://browser/skin/preferences/application.png";
+
+// For CSS. Can be one of "ask", "save" or "handleInternally". If absent, the icon URL
+// was set by us to a custom handler icon and CSS should not try to override it.
+const APP_ICON_ATTR_NAME = "appHandlerIcon";
+
+Preferences.addAll([
+ // Startup
+ { id: "browser.startup.page", type: "int" },
+ { id: "browser.privatebrowsing.autostart", type: "bool" },
+
+ // Downloads
+ { id: "browser.download.useDownloadDir", type: "bool", inverted: true },
+ { id: "browser.download.always_ask_before_handling_new_types", type: "bool" },
+ { id: "browser.download.folderList", type: "int" },
+ { id: "browser.download.dir", type: "file" },
+
+ /* Tab preferences
+ Preferences:
+
+ browser.link.open_newwindow
+ 1 opens such links in the most recent window or tab,
+ 2 opens such links in a new window,
+ 3 opens such links in a new tab
+ browser.tabs.loadInBackground
+ - true if display should switch to a new tab which has been opened from a
+ link, false if display shouldn't switch
+ browser.tabs.warnOnClose
+ - true if when closing a window with multiple tabs the user is warned and
+ allowed to cancel the action, false to just close the window
+ browser.tabs.warnOnOpen
+ - true if the user should be warned if he attempts to open a lot of tabs at
+ once (e.g. a large folder of bookmarks), false otherwise
+ browser.warnOnQuitShortcut
+ - true if the user should be warned if they quit using the keyboard shortcut
+ browser.taskbar.previews.enable
+ - true if tabs are to be shown in the Windows 7 taskbar
+ */
+
+ { id: "browser.link.open_newwindow", type: "int" },
+ { id: "browser.tabs.loadInBackground", type: "bool", inverted: true },
+ { id: "browser.tabs.warnOnClose", type: "bool" },
+ { id: "browser.warnOnQuitShortcut", type: "bool" },
+ { id: "browser.tabs.warnOnOpen", type: "bool" },
+ { id: "browser.ctrlTab.sortByRecentlyUsed", type: "bool" },
+
+ // CFR
+ {
+ id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
+ type: "bool",
+ },
+ {
+ id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ type: "bool",
+ },
+
+ // Fonts
+ { id: "font.language.group", type: "wstring" },
+
+ // Languages
+ { id: "browser.translation.detectLanguage", type: "bool" },
+ { id: "intl.regional_prefs.use_os_locales", type: "bool" },
+
+ // General tab
+
+ /* Accessibility
+ * accessibility.browsewithcaret
+ - true enables keyboard navigation and selection within web pages using a
+ visible caret, false uses normal keyboard navigation with no caret
+ * accessibility.typeaheadfind
+ - when set to true, typing outside text areas and input boxes will
+ automatically start searching for what's typed within the current
+ document; when set to false, no search action happens */
+ { id: "accessibility.browsewithcaret", type: "bool" },
+ { id: "accessibility.typeaheadfind", type: "bool" },
+ { id: "accessibility.blockautorefresh", type: "bool" },
+
+ /* Browsing
+ * general.autoScroll
+ - when set to true, clicking the scroll wheel on the mouse activates a
+ mouse mode where moving the mouse down scrolls the document downward with
+ speed correlated with the distance of the cursor from the original
+ position at which the click occurred (and likewise with movement upward);
+ if false, this behavior is disabled
+ * general.smoothScroll
+ - set to true to enable finer page scrolling than line-by-line on page-up,
+ page-down, and other such page movements */
+ { id: "general.autoScroll", type: "bool" },
+ { id: "general.smoothScroll", type: "bool" },
+ { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true },
+ { id: "layout.spellcheckDefault", type: "int" },
+
+ {
+ id: "browser.preferences.defaultPerformanceSettings.enabled",
+ type: "bool",
+ },
+ { id: "dom.ipc.processCount", type: "int" },
+ { id: "dom.ipc.processCount.web", type: "int" },
+ { id: "layers.acceleration.disabled", type: "bool", inverted: true },
+
+ // Files and Applications
+ { id: "pref.downloads.disable_button.edit_actions", type: "bool" },
+
+ // DRM content
+ { id: "media.eme.enabled", type: "bool" },
+
+ // Update
+ { id: "browser.preferences.advanced.selectedTabIndex", type: "int" },
+ { id: "browser.search.update", type: "bool" },
+
+ { id: "privacy.userContext.enabled", type: "bool" },
+ {
+ id: "privacy.userContext.newTabContainerOnLeftClick.enabled",
+ type: "bool",
+ },
+
+ // Picture-in-Picture
+ {
+ id: "media.videocontrols.picture-in-picture.video-toggle.enabled",
+ type: "bool",
+ },
+
+ // Media
+ { id: "media.hardwaremediakeys.enabled", type: "bool" },
+]);
+
+if (AppConstants.HAVE_SHELL_SERVICE) {
+ Preferences.addAll([
+ { id: "browser.shell.checkDefaultBrowser", type: "bool" },
+ { id: "pref.general.disable_button.default_browser", type: "bool" },
+ ]);
+}
+
+if (AppConstants.platform === "win") {
+ Preferences.addAll([
+ { id: "browser.taskbar.previews.enable", type: "bool" },
+ { id: "ui.osk.enabled", type: "bool" },
+ ]);
+}
+
+if (AppConstants.MOZ_UPDATER) {
+ Preferences.addAll([
+ { id: "app.update.disable_button.showUpdateHistory", type: "bool" },
+ ]);
+
+ if (AppConstants.NIGHTLY_BUILD) {
+ Preferences.addAll([{ id: "app.update.suppressPrompts", type: "bool" }]);
+ }
+
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]);
+ }
+}
+
+XPCOMUtils.defineLazyGetter(this, "gIsPackagedApp", () => {
+ return Services.sysinfo.getProperty("isPackagedApp");
+});
+
+// A promise that resolves when the list of application handlers is loaded.
+// We store this in a global so tests can await it.
+var promiseLoadHandlersList;
+
+// Load the preferences string bundle for other locales with fallbacks.
+function getBundleForLocales(newLocales) {
+ let locales = Array.from(
+ new Set([
+ ...newLocales,
+ ...Services.locale.requestedLocales,
+ Services.locale.lastFallbackLocale,
+ ])
+ );
+ return new Localization(
+ ["browser/preferences/preferences.ftl", "branding/brand.ftl"],
+ false,
+ undefined,
+ locales
+ );
+}
+
+var gNodeToObjectMap = new WeakMap();
+
+var gMainPane = {
+ // The set of types the app knows how to handle. A hash of HandlerInfoWrapper
+ // objects, indexed by type.
+ _handledTypes: {},
+
+ // The list of types we can show, sorted by the sort column/direction.
+ // An array of HandlerInfoWrapper objects. We build this list when we first
+ // load the data and then rebuild it when users change a pref that affects
+ // what types we can show or change the sort column/direction.
+ // Note: this isn't necessarily the list of types we *will* show; if the user
+ // provides a filter string, we'll only show the subset of types in this list
+ // that match that string.
+ _visibleTypes: [],
+
+ // browser.startup.page values
+ STARTUP_PREF_BLANK: 0,
+ STARTUP_PREF_HOMEPAGE: 1,
+ STARTUP_PREF_RESTORE_SESSION: 3,
+
+ // Convenience & Performance Shortcuts
+
+ get _list() {
+ delete this._list;
+ return (this._list = document.getElementById("handlersView"));
+ },
+
+ get _filter() {
+ delete this._filter;
+ return (this._filter = document.getElementById("filter"));
+ },
+
+ _backoffIndex: 0,
+
+ /**
+ * Initialization of gMainPane.
+ */
+ init() {
+ function setEventListener(aId, aEventType, aCallback) {
+ document
+ .getElementById(aId)
+ .addEventListener(aEventType, aCallback.bind(gMainPane));
+ }
+
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ this.updateSetDefaultBrowser();
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // Exponential backoff mechanism will delay the polling times if user doesn't
+ // trigger SetDefaultBrowser for a long time.
+ let backoffTimes = [
+ 1000, 1000, 1000, 1000, 2000, 2000, 2000, 5000, 5000, 10000,
+ ];
+
+ let pollForDefaultBrowser = () => {
+ let uri = win.gBrowser.currentURI.spec;
+
+ if (
+ (uri == "about:preferences" || uri == "about:preferences#general") &&
+ document.visibilityState == "visible"
+ ) {
+ this.updateSetDefaultBrowser();
+ }
+
+ // approximately a "requestIdleInterval"
+ window.setTimeout(() => {
+ window.requestIdleCallback(pollForDefaultBrowser);
+ }, backoffTimes[this._backoffIndex + 1 < backoffTimes.length ? this._backoffIndex++ : backoffTimes.length - 1]);
+ };
+
+ window.setTimeout(() => {
+ window.requestIdleCallback(pollForDefaultBrowser);
+ }, backoffTimes[this._backoffIndex]);
+ }
+
+ this.initBrowserContainers();
+ this.buildContentProcessCountMenuList();
+
+ this.updateDefaultPerformanceSettingsPref();
+
+ let defaultPerformancePref = Preferences.get(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ );
+ defaultPerformancePref.on("change", () => {
+ this.updatePerformanceSettingsBox({ duringChangeEvent: true });
+ });
+ this.updatePerformanceSettingsBox({ duringChangeEvent: false });
+ this.displayUseSystemLocale();
+ this.updateProxySettingsUI();
+ initializeProxyUI(gMainPane);
+
+ if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
+ gMainPane.initPrimaryBrowserLanguageUI();
+ }
+
+ // We call `initDefaultZoomValues` to set and unhide the
+ // default zoom preferences menu, and to establish a
+ // listener for future menu changes.
+ gMainPane.initDefaultZoomValues();
+
+ gMainPane.initTranslations();
+
+ if (
+ Services.prefs.getBoolPref(
+ "media.videocontrols.picture-in-picture.enabled"
+ )
+ ) {
+ document.getElementById("pictureInPictureBox").hidden = false;
+ setEventListener(
+ "pictureInPictureToggleEnabled",
+ "command",
+ function (event) {
+ if (!event.target.checked) {
+ Services.telemetry.recordEvent(
+ "pictureinpicture.settings",
+ "disable",
+ "settings"
+ );
+ }
+ }
+ );
+ }
+
+ if (AppConstants.platform == "win") {
+ // Functionality for "Show tabs in taskbar" on Windows 7 and up.
+ try {
+ let ver = parseFloat(Services.sysinfo.getProperty("version"));
+ let showTabsInTaskbar = document.getElementById("showTabsInTaskbar");
+ showTabsInTaskbar.hidden = ver < 6.1;
+ } catch (ex) {}
+ }
+
+ // The "opening multiple tabs might slow down Firefox" warning provides
+ // an option for not showing this warning again. When the user disables it,
+ // we provide checkboxes to re-enable the warning.
+ if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) {
+ document.getElementById("warnOpenMany").hidden = true;
+ }
+
+ if (AppConstants.platform != "win") {
+ let quitKeyElement =
+ window.browsingContext.topChromeWindow.document.getElementById(
+ "key_quitApplication"
+ );
+ if (quitKeyElement) {
+ let quitKey = ShortcutUtils.prettifyShortcut(quitKeyElement);
+ document.l10n.setAttributes(
+ document.getElementById("warnOnQuitKey"),
+ "confirm-on-quit-with-key",
+ { quitKey }
+ );
+ } else {
+ // If the quit key element does not exist, then the quit key has
+ // been disabled, so just hide the checkbox.
+ document.getElementById("warnOnQuitKey").hidden = true;
+ }
+ }
+
+ setEventListener("ctrlTabRecentlyUsedOrder", "command", function () {
+ Services.prefs.clearUserPref("browser.ctrlTab.migrated");
+ });
+ setEventListener("manageBrowserLanguagesButton", "command", function () {
+ gMainPane.showBrowserLanguagesSubDialog({ search: false });
+ });
+ if (AppConstants.MOZ_UPDATER) {
+ // These elements are only compiled in when the updater is enabled
+ setEventListener("checkForUpdatesButton", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ setEventListener("downloadAndInstallButton", "command", function () {
+ gAppUpdater.startDownload();
+ });
+ setEventListener("updateButton", "command", function () {
+ gAppUpdater.buttonRestartAfterDownload();
+ });
+ setEventListener("checkForUpdatesButton2", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ setEventListener("checkForUpdatesButton3", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ setEventListener("checkForUpdatesButton4", "command", function () {
+ gAppUpdater.checkForUpdates();
+ });
+ }
+
+ // Startup pref
+ setEventListener(
+ "browserRestoreSession",
+ "command",
+ gMainPane.onBrowserRestoreSessionChange
+ );
+ gMainPane.updateBrowserStartupUI =
+ gMainPane.updateBrowserStartupUI.bind(gMainPane);
+ Preferences.get("browser.privatebrowsing.autostart").on(
+ "change",
+ gMainPane.updateBrowserStartupUI
+ );
+ Preferences.get("browser.startup.page").on(
+ "change",
+ gMainPane.updateBrowserStartupUI
+ );
+ Preferences.get("browser.startup.homepage").on(
+ "change",
+ gMainPane.updateBrowserStartupUI
+ );
+ gMainPane.updateBrowserStartupUI();
+
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ setEventListener(
+ "setDefaultButton",
+ "command",
+ gMainPane.setDefaultBrowser
+ );
+ }
+ setEventListener(
+ "disableContainersExtension",
+ "command",
+ makeDisableControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY)
+ );
+ setEventListener("chooseLanguage", "command", gMainPane.showLanguages);
+ setEventListener(
+ "translationAttributionImage",
+ "click",
+ gMainPane.openTranslationProviderAttribution
+ );
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ setEventListener(
+ "translateButton",
+ "command",
+ gMainPane.showTranslationExceptions
+ );
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ setEventListener(
+ "fxtranslateButton",
+ "command",
+ gMainPane.showTranslationExceptions
+ );
+ Preferences.get("font.language.group").on(
+ "change",
+ gMainPane._rebuildFonts.bind(gMainPane)
+ );
+ setEventListener("advancedFonts", "command", gMainPane.configureFonts);
+ setEventListener("colors", "command", gMainPane.configureColors);
+ Preferences.get("layers.acceleration.disabled").on(
+ "change",
+ gMainPane.updateHardwareAcceleration.bind(gMainPane)
+ );
+ setEventListener(
+ "connectionSettings",
+ "command",
+ gMainPane.showConnections
+ );
+ setEventListener(
+ "browserContainersCheckbox",
+ "command",
+ gMainPane.checkBrowserContainers
+ );
+ setEventListener(
+ "browserContainersSettings",
+ "command",
+ gMainPane.showContainerSettings
+ );
+ setEventListener(
+ "data-migration",
+ "command",
+ gMainPane.onMigrationButtonCommand
+ );
+
+ document
+ .getElementById("migrationWizardDialog")
+ .addEventListener("MigrationWizard:Close", function (e) {
+ e.currentTarget.close();
+ });
+
+ if (Services.policies && !Services.policies.isAllowed("profileImport")) {
+ document.getElementById("dataMigrationGroup").remove();
+ }
+
+ // For media control toggle button, we support it on Windows 8.1+ (NT6.3),
+ // MacOs 10.4+ (darwin8.0, but we already don't support that) and
+ // gtk-based Linux.
+ if (
+ AppConstants.isPlatformAndVersionAtLeast("win", "6.3") ||
+ AppConstants.platform == "macosx" ||
+ AppConstants.MOZ_WIDGET_GTK
+ ) {
+ document.getElementById("mediaControlBox").hidden = false;
+ }
+
+ // Initializes the fonts dropdowns displayed in this pane.
+ this._rebuildFonts();
+
+ this.updateOnScreenKeyboardVisibility();
+
+ // Show translation preferences if we may:
+ const translationsPrefName = "browser.translation.ui.show";
+ if (Services.prefs.getBoolPref(translationsPrefName)) {
+ let row = document.getElementById("translationBox");
+ row.removeAttribute("hidden");
+ // Showing attribution only for Bing Translator.
+ var { Translation } = ChromeUtils.import(
+ "resource:///modules/translation/TranslationParent.jsm"
+ );
+ if (Translation.translationEngine == "Bing") {
+ document.getElementById("bingAttribution").removeAttribute("hidden");
+ }
+ }
+
+ // Firefox Translations settings panel
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ const fxtranslationsDisabledPrefName = "extensions.translations.disabled";
+ if (!Services.prefs.getBoolPref(fxtranslationsDisabledPrefName, true)) {
+ let fxtranslationRow = document.getElementById("fxtranslationsBox");
+ fxtranslationRow.hidden = false;
+ }
+
+ let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled");
+ // Force-disable/hide on WinXP:
+ if (navigator.platform.toLowerCase().startsWith("win")) {
+ emeUIEnabled =
+ emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
+ }
+ if (!emeUIEnabled) {
+ // Don't want to rely on .hidden for the toplevel groupbox because
+ // of the pane hiding/showing code potentially interfering:
+ document
+ .getElementById("drmGroup")
+ .setAttribute("style", "display: none !important");
+ }
+ // Initialize the Firefox Updates section.
+ let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
+
+ // Include the build ID if this is an "a#" (nightly) build
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let year = buildID.slice(0, 4);
+ let month = buildID.slice(4, 6);
+ let day = buildID.slice(6, 8);
+ version += ` (${year}-${month}-${day})`;
+ }
+
+ // Append "(32-bit)" or "(64-bit)" build architecture to the version number:
+ let bundle = Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+ let archResource = Services.appinfo.is64Bit
+ ? "aboutDialog.architecture.sixtyFourBit"
+ : "aboutDialog.architecture.thirtyTwoBit";
+ let arch = bundle.GetStringFromName(archResource);
+ version += ` (${arch})`;
+
+ document.l10n.setAttributes(
+ document.getElementById("updateAppInfo"),
+ "update-application-version",
+ { version }
+ );
+
+ // Show a release notes link if we have a URL.
+ let relNotesLink = document.getElementById("releasenotes");
+ let relNotesPrefType = Services.prefs.getPrefType("app.releaseNotesURL");
+ if (relNotesPrefType != Services.prefs.PREF_INVALID) {
+ let relNotesURL = Services.urlFormatter.formatURLPref(
+ "app.releaseNotesURL"
+ );
+ if (relNotesURL != "about:blank") {
+ relNotesLink.href = relNotesURL;
+ relNotesLink.hidden = false;
+ }
+ }
+
+ let defaults = Services.prefs.getDefaultBranch(null);
+ let distroId = defaults.getCharPref("distribution.id", "");
+ if (distroId) {
+ let distroString = distroId;
+
+ let distroVersion = defaults.getCharPref("distribution.version", "");
+ if (distroVersion) {
+ distroString += " - " + distroVersion;
+ }
+
+ let distroIdField = document.getElementById("distributionId");
+ distroIdField.value = distroString;
+ distroIdField.hidden = false;
+
+ let distroAbout = defaults.getStringPref("distribution.about", "");
+ if (distroAbout) {
+ let distroField = document.getElementById("distribution");
+ distroField.value = distroAbout;
+ distroField.hidden = false;
+ }
+ }
+
+ if (AppConstants.MOZ_UPDATER) {
+ gAppUpdater = new appUpdater();
+ setEventListener("showUpdateHistory", "command", gMainPane.showUpdates);
+
+ let updateDisabled =
+ Services.policies && !Services.policies.isAllowed("appUpdate");
+
+ if (gIsPackagedApp) {
+ // When we're running inside an app package, there's no point in
+ // displaying any update content here, and it would get confusing if we
+ // did, because our updater is not enabled.
+ // We can't rely on the hidden attribute for the toplevel elements,
+ // because of the pane hiding/showing code interfering.
+ document
+ .getElementById("updatesCategory")
+ .setAttribute("style", "display: none !important");
+ document
+ .getElementById("updateApp")
+ .setAttribute("style", "display: none !important");
+ } else if (
+ updateDisabled ||
+ UpdateUtils.appUpdateAutoSettingIsLocked() ||
+ gApplicationUpdateService.manualUpdateOnly
+ ) {
+ document.getElementById("updateAllowDescription").hidden = true;
+ document.getElementById("updateSettingsContainer").hidden = true;
+ if (updateDisabled && AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ document.getElementById("useService").hidden = true;
+ }
+ } else {
+ // Start with no option selected since we are still reading the value
+ document.getElementById("autoDesktop").removeAttribute("selected");
+ document.getElementById("manualDesktop").removeAttribute("selected");
+ // Start reading the correct value from the disk
+ this.readUpdateAutoPref();
+ setEventListener("updateRadioGroup", "command", event => {
+ if (event.target.id == "backgroundUpdate") {
+ this.writeBackgroundUpdatePref();
+ } else {
+ this.writeUpdateAutoPref();
+ }
+ });
+ if (this.isBackgroundUpdateUIAvailable()) {
+ document.getElementById("backgroundUpdate").hidden = false;
+ // Start reading the background update pref's value from the disk.
+ this.readBackgroundUpdatePref();
+ }
+ }
+
+ if (AppConstants.platform == "win") {
+ // On Windows, the Application Update setting is an installation-
+ // specific preference, not a profile-specific one. Show a warning to
+ // inform users of this.
+ let updateContainer = document.getElementById(
+ "updateSettingsContainer"
+ );
+ updateContainer.classList.add("updateSettingCrossUserWarningContainer");
+ document.getElementById(
+ "updateSettingCrossUserWarningDesc"
+ ).hidden = false;
+ }
+
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ // Check to see if the maintenance service is installed.
+ // If it isn't installed, don't show the preference at all.
+ let installed;
+ try {
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ wrk.open(
+ wrk.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\MaintenanceService",
+ wrk.ACCESS_READ | wrk.WOW64_64
+ );
+ installed = wrk.readIntValue("Installed");
+ wrk.close();
+ } catch (e) {}
+ if (installed != 1) {
+ document.getElementById("useService").hidden = true;
+ }
+ }
+ }
+
+ // Initilize Application section.
+
+ // Observe preferences that influence what we display so we can rebuild
+ // the view when they change.
+ Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
+ Services.obs.addObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
+
+ setEventListener("filter", "command", gMainPane.filter);
+ setEventListener("typeColumn", "click", gMainPane.sort);
+ setEventListener("actionColumn", "click", gMainPane.sort);
+ setEventListener("chooseFolder", "command", gMainPane.chooseFolder);
+ Preferences.get("browser.download.folderList").on(
+ "change",
+ gMainPane.displayDownloadDirPref.bind(gMainPane)
+ );
+ Preferences.get("browser.download.dir").on(
+ "change",
+ gMainPane.displayDownloadDirPref.bind(gMainPane)
+ );
+ gMainPane.displayDownloadDirPref();
+
+ // Listen for window unload so we can remove our preference observers.
+ window.addEventListener("unload", this);
+
+ // Figure out how we should be sorting the list. We persist sort settings
+ // across sessions, so we can't assume the default sort column/direction.
+ // XXX should we be using the XUL sort service instead?
+ if (document.getElementById("actionColumn").hasAttribute("sortDirection")) {
+ this._sortColumn = document.getElementById("actionColumn");
+ // The typeColumn element always has a sortDirection attribute,
+ // either because it was persisted or because the default value
+ // from the xul file was used. If we are sorting on the other
+ // column, we should remove it.
+ document.getElementById("typeColumn").removeAttribute("sortDirection");
+ } else {
+ this._sortColumn = document.getElementById("typeColumn");
+ }
+
+ let browserBundle = document.getElementById("browserBundle");
+ appendSearchKeywords("browserContainersSettings", [
+ browserBundle.getString("userContextPersonal.label"),
+ browserBundle.getString("userContextWork.label"),
+ browserBundle.getString("userContextBanking.label"),
+ browserBundle.getString("userContextShopping.label"),
+ ]);
+
+ AppearanceChooser.init();
+
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "main-pane-loaded");
+
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("defaultFont"),
+ element => FontBuilder.readFontSelection(element)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("translate"),
+ () =>
+ this.updateButtons(
+ "translateButton",
+ "browser.translation.detectLanguage"
+ )
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("checkSpelling"),
+ () => this.readCheckSpelling()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("checkSpelling"),
+ () => this.writeCheckSpelling()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("alwaysAsk"),
+ () => this.readUseDownloadDir()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("linkTargeting"),
+ () => this.readLinkTarget()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("linkTargeting"),
+ () => this.writeLinkTarget()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("browserContainersCheckbox"),
+ () => this.readBrowserContainersCheckbox()
+ );
+
+ this.setInitialized();
+ },
+
+ preInit() {
+ promiseLoadHandlersList = new Promise((resolve, reject) => {
+ // Load the data and build the list of handlers for applications pane.
+ // By doing this after pageshow, we ensure it doesn't delay painting
+ // of the preferences page.
+ window.addEventListener(
+ "pageshow",
+ async () => {
+ await this.initialized;
+ try {
+ this._initListEventHandlers();
+ this._loadData();
+ await this._rebuildVisibleTypes();
+ await this._rebuildView();
+ await this._sortListView();
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ },
+ { once: true }
+ );
+ });
+ },
+
+ handleSubcategory(subcategory) {
+ if (Services.policies && !Services.policies.isAllowed("profileImport")) {
+ return false;
+ }
+ if (subcategory == "migrate") {
+ this.showMigrationWizardDialog();
+ return true;
+ }
+
+ if (subcategory == "migrate-autoclose") {
+ this.showMigrationWizardDialog({ closeTabWhenDone: true });
+ }
+
+ return false;
+ },
+
+ // CONTAINERS
+
+ /*
+ * preferences:
+ *
+ * privacy.userContext.enabled
+ * - true if containers is enabled
+ */
+
+ /**
+ * Enables/disables the Settings button used to configure containers
+ */
+ readBrowserContainersCheckbox() {
+ const pref = Preferences.get("privacy.userContext.enabled");
+ const settings = document.getElementById("browserContainersSettings");
+
+ settings.disabled = !pref.value;
+ const containersEnabled = Services.prefs.getBoolPref(
+ "privacy.userContext.enabled"
+ );
+ const containersCheckbox = document.getElementById(
+ "browserContainersCheckbox"
+ );
+ containersCheckbox.checked = containersEnabled;
+ handleControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY).then(
+ isControlled => {
+ containersCheckbox.disabled = isControlled;
+ }
+ );
+ },
+
+ /**
+ * Show the Containers UI depending on the privacy.userContext.ui.enabled pref.
+ */
+ initBrowserContainers() {
+ if (!Services.prefs.getBoolPref("privacy.userContext.ui.enabled")) {
+ // The browserContainersGroup element has its own internal padding that
+ // is visible even if the browserContainersbox is visible, so hide the whole
+ // groupbox if the feature is disabled to prevent a gap in the preferences.
+ document
+ .getElementById("browserContainersbox")
+ .setAttribute("data-hidden-from-search", "true");
+ return;
+ }
+ Services.prefs.addObserver(PREF_CONTAINERS_EXTENSION, this);
+
+ document.getElementById("browserContainersbox").hidden = false;
+ this.readBrowserContainersCheckbox();
+ },
+
+ async onGetStarted(aEvent) {
+ if (!AppConstants.MOZ_DEV_EDITION) {
+ return;
+ }
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!win) {
+ return;
+ }
+ const user = await fxAccounts.getSignedInUser();
+ if (user) {
+ // We have a user, open Sync preferences in the same tab
+ win.openTrustedLinkIn("about:preferences#sync", "current");
+ return;
+ }
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+ let url = await FxAccounts.config.promiseConnectAccountURI(
+ "dev-edition-setup"
+ );
+ let accountsTab = win.gBrowser.addWebTab(url);
+ win.gBrowser.selectedTab = accountsTab;
+ },
+
+ // HOME PAGE
+ /*
+ * Preferences:
+ *
+ * browser.startup.page
+ * - what page(s) to show when the user starts the application, as an integer:
+ *
+ * 0: a blank page (DEPRECATED - this can be set via browser.startup.homepage)
+ * 1: the home page (as set by the browser.startup.homepage pref)
+ * 2: the last page the user visited (DEPRECATED)
+ * 3: windows and tabs from the last session (a.k.a. session restore)
+ *
+ * The deprecated option is not exposed in UI; however, if the user has it
+ * selected and doesn't change the UI for this preference, the deprecated
+ * option is preserved.
+ */
+
+ /**
+ * Utility function to enable/disable the button specified by aButtonID based
+ * on the value of the Boolean preference specified by aPreferenceID.
+ */
+ updateButtons(aButtonID, aPreferenceID) {
+ var button = document.getElementById(aButtonID);
+ var preference = Preferences.get(aPreferenceID);
+ button.disabled = !preference.value;
+ return undefined;
+ },
+
+ /**
+ * Hide/show the "Show my windows and tabs from last time" option based
+ * on the value of the browser.privatebrowsing.autostart pref.
+ */
+ updateBrowserStartupUI() {
+ const pbAutoStartPref = Preferences.get(
+ "browser.privatebrowsing.autostart"
+ );
+ const startupPref = Preferences.get("browser.startup.page");
+
+ let newValue;
+ let checkbox = document.getElementById("browserRestoreSession");
+ checkbox.disabled = pbAutoStartPref.value || startupPref.locked;
+ newValue = pbAutoStartPref.value
+ ? false
+ : startupPref.value === this.STARTUP_PREF_RESTORE_SESSION;
+ if (checkbox.checked !== newValue) {
+ checkbox.checked = newValue;
+ }
+ },
+ /**
+ * Fetch the existing default zoom value, initialise and unhide
+ * the preferences menu. This method also establishes a listener
+ * to ensure handleDefaultZoomChange is called on future menu
+ * changes.
+ */
+ async initDefaultZoomValues() {
+ let win = window.browsingContext.topChromeWindow;
+ let selected = await win.ZoomUI.getGlobalValue();
+ let menulist = document.getElementById("defaultZoom");
+
+ new SelectionChangedMenulist(menulist, event => {
+ let parsedZoom = parseFloat((event.target.value / 100).toFixed(2));
+ gMainPane.handleDefaultZoomChange(parsedZoom);
+ });
+
+ setEventListener("zoomText", "command", function () {
+ win.ZoomManager.toggleZoom();
+ });
+
+ let zoomValues = win.ZoomManager.zoomValues.map(a => {
+ return Math.round(a * 100);
+ });
+
+ let fragment = document.createDocumentFragment();
+ for (let zoomLevel of zoomValues) {
+ let menuitem = document.createXULElement("menuitem");
+ document.l10n.setAttributes(menuitem, "preferences-default-zoom-value", {
+ percentage: zoomLevel,
+ });
+ menuitem.setAttribute("value", zoomLevel);
+ fragment.appendChild(menuitem);
+ }
+
+ let menupopup = menulist.querySelector("menupopup");
+ menupopup.appendChild(fragment);
+ menulist.value = Math.round(selected * 100);
+
+ let checkbox = document.getElementById("zoomText");
+ checkbox.checked = !win.ZoomManager.useFullZoom;
+
+ document.getElementById("zoomBox").hidden = false;
+ },
+
+ /**
+ * Initialize the translations view.
+ */
+ async initTranslations() {
+ if (!Services.prefs.getBoolPref("browser.translations.enable")) {
+ return;
+ }
+
+ /**
+ * Which phase a language download is in.
+ *
+ * @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase
+ */
+
+ // Immediately show the group so that the async load of the component does
+ // not cause the layout to jump. The group will be empty initially.
+ document.getElementById("translationsGroup").hidden = false;
+
+ class TranslationsState {
+ /**
+ * The fully initialized state.
+ *
+ * @param {TranslationsActor} translationsActor
+ * @param {Object} supportedLanguages
+ * @param {Array<{ langTag: string, displayName: string}} languageList
+ * @param {Map<string, DownloadPhase>} downloadPhases
+ */
+ constructor(
+ translationsActor,
+ supportedLanguages,
+ languageList,
+ downloadPhases
+ ) {
+ this.translationsActor = translationsActor;
+ this.supportedLanguages = supportedLanguages;
+ this.languageList = languageList;
+ this.downloadPhases = downloadPhases;
+ }
+
+ /**
+ * Handles all of the async initialization logic.
+ */
+ static async create() {
+ const translationsActor =
+ window.windowGlobalChild.getActor("Translations");
+ const supportedLanguages =
+ await translationsActor.getSupportedLanguages();
+ const languageList =
+ TranslationsState.getLanguageList(supportedLanguages);
+ const downloadPhases = await TranslationsState.createDownloadPhases(
+ translationsActor,
+ languageList
+ );
+
+ if (supportedLanguages.languagePairs.length === 0) {
+ throw new Error(
+ "The supported languages list was empty. RemoteSettings may not be available at the moment."
+ );
+ }
+
+ return new TranslationsState(
+ translationsActor,
+ supportedLanguages,
+ languageList,
+ downloadPhases
+ );
+ }
+
+ /**
+ * Create a unique list of languages, sorted by the display name.
+ *
+ * @param {Object} supportedLanguages
+ * @returns {Array<{ langTag: string, displayName: string}}
+ */
+ static getLanguageList(supportedLanguages) {
+ const displayNames = new Map();
+ for (const languages of [
+ supportedLanguages.fromLanguages,
+ supportedLanguages.toLanguages,
+ ]) {
+ for (const { langTag, displayName } of languages) {
+ displayNames.set(langTag, displayName);
+ }
+ }
+
+ let appLangTag = new Intl.Locale(Services.locale.appLocaleAsBCP47)
+ .language;
+
+ // Don't offer to download the app's language.
+ displayNames.delete(appLangTag);
+
+ // Sort the list of languages by the display names.
+ return [...displayNames.entries()]
+ .map(([langTag, displayName]) => ({
+ langTag,
+ displayName,
+ }))
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
+ }
+
+ /**
+ * Determine the download phase of each language file.
+ *
+ * @param {TranslationsChild} translationsActor
+ * @param {Array<{ langTag: string, displayName: string}} languageList.
+ * @returns {Map<string, DownloadPhase>} Map the language tag to whether it is downloaded.
+ */
+ static async createDownloadPhases(translationsActor, languageList) {
+ const downloadPhases = new Map();
+ for (const { langTag } of languageList) {
+ downloadPhases.set(
+ langTag,
+ (await translationsActor.hasAllFilesForLanguage(langTag))
+ ? "downloaded"
+ : "uninstalled"
+ );
+ }
+ return downloadPhases;
+ }
+ }
+
+ class TranslationsView {
+ /** @type {Map<string, XULButton>} */
+ deleteButtons = new Map();
+ /** @type {Map<string, XULButton>} */
+ downloadButtons = new Map();
+
+ /**
+ * @param {TranslationsState} state
+ */
+ constructor(state) {
+ this.state = state;
+ this.elements = {
+ settingsButton: document.getElementById(
+ "translations-manage-settings-button"
+ ),
+ installList: document.getElementById(
+ "translations-manage-install-list"
+ ),
+ installAll: document.getElementById(
+ "translations-manage-install-all"
+ ),
+ deleteAll: document.getElementById("translations-manage-delete-all"),
+ error: document.getElementById("translations-manage-error"),
+ };
+ this.setup();
+ }
+
+ setup() {
+ this.buildLanguageList();
+
+ this.elements.settingsButton.addEventListener(
+ "command",
+ gMainPane.showTranslationsSettings
+ );
+ this.elements.installAll.addEventListener(
+ "command",
+ this.handleInstallAll
+ );
+ this.elements.deleteAll.addEventListener(
+ "command",
+ this.handleDeleteAll
+ );
+ }
+
+ handleInstallAll = async () => {
+ this.hideError();
+ this.disableButtons(true);
+ try {
+ await this.state.translationsActor.downloadAllFiles();
+ this.markAllDownloadPhases("downloaded");
+ } catch (error) {
+ TranslationsView.showError(
+ "translations-manage-error-download",
+ error
+ );
+ await this.reloadDownloadPhases();
+ this.updateAllButtons();
+ }
+ this.disableButtons(false);
+ };
+
+ handleDeleteAll = async () => {
+ this.hideError();
+ this.disableButtons(true);
+ try {
+ await this.state.translationsActor.deleteAllLanguageFiles();
+ this.markAllDownloadPhases("uninstalled");
+ } catch (error) {
+ TranslationsView.showError("translations-manage-error-delete", error);
+ // The download phases are invalidated with the error and must be reloaded.
+ await this.reloadDownloadPhases();
+ console.error(error);
+ }
+ this.disableButtons(false);
+ };
+
+ /**
+ * @param {string} langTag
+ * @returns {Function}
+ */
+ getDownloadButtonHandler(langTag) {
+ return async () => {
+ this.hideError();
+ this.updateDownloadPhase(langTag, "loading");
+ try {
+ await this.state.translationsActor.downloadLanguageFiles(langTag);
+ this.updateDownloadPhase(langTag, "downloaded");
+ } catch (error) {
+ TranslationsView.showError(
+ "translations-manage-error-download",
+ error
+ );
+ this.updateDownloadPhase(langTag, "uninstalled");
+ }
+ };
+ }
+
+ /**
+ * @param {string} langTag
+ * @returns {Function}
+ */
+ getDeleteButtonHandler(langTag) {
+ return async () => {
+ this.hideError();
+ this.updateDownloadPhase(langTag, "loading");
+ try {
+ await this.state.translationsActor.deleteLanguageFiles(langTag);
+ this.updateDownloadPhase(langTag, "uninstalled");
+ } catch (error) {
+ TranslationsView.showError(
+ "translations-manage-error-delete",
+ error
+ );
+ // The download phases are invalidated with the error and must be reloaded.
+ await this.reloadDownloadPhases();
+ }
+ };
+ }
+
+ buildLanguageList() {
+ const listFragment = document.createDocumentFragment();
+
+ for (const { langTag, displayName } of this.state.languageList) {
+ const hboxRow = document.createXULElement("hbox");
+ hboxRow.classList.add("translations-manage-language");
+
+ const languageLabel = document.createXULElement("label");
+ languageLabel.textContent = displayName; // The display name is already localized.
+
+ const downloadButton = document.createXULElement("button");
+ const deleteButton = document.createXULElement("button");
+
+ downloadButton.addEventListener(
+ "command",
+ this.getDownloadButtonHandler(langTag)
+ );
+ deleteButton.addEventListener(
+ "command",
+ this.getDeleteButtonHandler(langTag)
+ );
+
+ document.l10n.setAttributes(
+ downloadButton,
+ "translations-manage-download-button"
+ );
+ document.l10n.setAttributes(
+ deleteButton,
+ "translations-manage-delete-button"
+ );
+
+ downloadButton.hidden = true;
+ deleteButton.hidden = true;
+
+ this.deleteButtons.set(langTag, deleteButton);
+ this.downloadButtons.set(langTag, downloadButton);
+
+ hboxRow.appendChild(languageLabel);
+ hboxRow.appendChild(downloadButton);
+ hboxRow.appendChild(deleteButton);
+ listFragment.appendChild(hboxRow);
+ }
+ this.updateAllButtons();
+ this.elements.installList.appendChild(listFragment);
+ this.elements.installList.hidden = false;
+ }
+
+ /**
+ * Update the DownloadPhase for a single langTag.
+ * @param {string} langTag
+ * @param {DownloadPhase} downloadPhase
+ */
+ updateDownloadPhase(langTag, downloadPhase) {
+ this.state.downloadPhases.set(langTag, downloadPhase);
+ this.updateButton(langTag, downloadPhase);
+ this.updateHeaderButtons();
+ }
+
+ /**
+ * Recreates the download map when the state is invalidated.
+ */
+ async reloadDownloadPhases() {
+ this.state.downloadPhases =
+ await TranslationsState.createDownloadPhases(
+ this.state.translationsActor,
+ this.state.languageList
+ );
+ this.updateAllButtons();
+ }
+
+ /**
+ * Set all the downloads.
+ * @param {DownloadPhase} downloadPhase
+ */
+ markAllDownloadPhases(downloadPhase) {
+ const { downloadPhases } = this.state;
+ for (const key of downloadPhases.keys()) {
+ downloadPhases.set(key, downloadPhase);
+ }
+ this.updateAllButtons();
+ }
+
+ /**
+ * If all languages are downloaded, or no languages are downloaded then
+ * the visibility of the buttons need to change.
+ */
+ updateHeaderButtons() {
+ let allDownloaded = true;
+ let allUninstalled = true;
+ for (const downloadPhase of this.state.downloadPhases.values()) {
+ if (downloadPhase === "loading") {
+ // Don't count loading towards this calculation.
+ continue;
+ }
+ allDownloaded &&= downloadPhase === "downloaded";
+ allUninstalled &&= downloadPhase === "uninstalled";
+ }
+
+ this.elements.installAll.hidden = allDownloaded;
+ this.elements.deleteAll.hidden = allUninstalled;
+ }
+
+ /**
+ * Update the buttons according to their download state.
+ */
+ updateAllButtons() {
+ this.updateHeaderButtons();
+ for (const [langTag, downloadPhase] of this.state.downloadPhases) {
+ this.updateButton(langTag, downloadPhase);
+ }
+ }
+
+ /**
+ * @param {string} langTag
+ * @param {DownloadPhase} downloadPhase
+ */
+ updateButton(langTag, downloadPhase) {
+ const downloadButton = this.downloadButtons.get(langTag);
+ const deleteButton = this.deleteButtons.get(langTag);
+ switch (downloadPhase) {
+ case "downloaded":
+ downloadButton.hidden = true;
+ deleteButton.hidden = false;
+ downloadButton.removeAttribute("disabled");
+ break;
+ case "uninstalled":
+ downloadButton.hidden = false;
+ deleteButton.hidden = true;
+ downloadButton.removeAttribute("disabled");
+ break;
+ case "loading":
+ downloadButton.hidden = false;
+ deleteButton.hidden = true;
+ downloadButton.setAttribute("disabled", true);
+ break;
+ }
+ }
+
+ /**
+ * @param {boolean} isDisabled
+ */
+ disableButtons(isDisabled) {
+ this.elements.installAll.disabled = isDisabled;
+ this.elements.deleteAll.disabled = isDisabled;
+ for (const button of this.downloadButtons.values()) {
+ button.disabled = isDisabled;
+ }
+ for (const button of this.deleteButtons.values()) {
+ button.disabled = isDisabled;
+ }
+ }
+
+ /**
+ * This method is static in case an error happens during the creation of the
+ * TranslationsState.
+ *
+ * @param {string} l10nId
+ * @param {Error} error
+ */
+ static showError(l10nId, error) {
+ console.error(error);
+ const errorMessage = document.getElementById(
+ "translations-manage-error"
+ );
+ errorMessage.hidden = false;
+ document.l10n.setAttributes(errorMessage, l10nId);
+ }
+
+ hideError() {
+ this.elements.error.hidden = true;
+ }
+ }
+
+ TranslationsState.create().then(
+ state => {
+ new TranslationsView(state);
+ },
+ error => {
+ // This error can happen when a user is not connected to the internet, or
+ // RemoteSettings is down for some reason.
+ TranslationsView.showError("translations-manage-error-list", error);
+ }
+ );
+ },
+
+ initPrimaryBrowserLanguageUI() {
+ // Enable telemetry.
+ Services.telemetry.setEventRecordingEnabled(
+ "intl.ui.browserLanguage",
+ true
+ );
+
+ // This will register the "command" listener.
+ let menulist = document.getElementById("primaryBrowserLocale");
+ new SelectionChangedMenulist(menulist, event => {
+ gMainPane.onPrimaryBrowserLanguageMenuChange(event);
+ });
+
+ gMainPane.updatePrimaryBrowserLanguageUI(Services.locale.appLocaleAsBCP47);
+ },
+
+ /**
+ * Update the available list of locales and select the locale that the user
+ * is "selecting". This could be the currently requested locale or a locale
+ * that the user would like to switch to after confirmation.
+ *
+ * @param {string} selected - The selected BCP 47 locale.
+ */
+ async updatePrimaryBrowserLanguageUI(selected) {
+ let available = await LangPackMatcher.getAvailableLocales();
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ available,
+ { preferNative: true }
+ );
+ let locales = available.map((code, i) => ({ code, name: localeNames[i] }));
+ locales.sort((a, b) => a.name > b.name);
+
+ let fragment = document.createDocumentFragment();
+ for (let { code, name } of locales) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("value", code);
+ menuitem.setAttribute("label", name);
+ fragment.appendChild(menuitem);
+ }
+
+ // Add an option to search for more languages if downloading is supported.
+ if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.id = "primaryBrowserLocaleSearch";
+ menuitem.setAttribute(
+ "label",
+ await document.l10n.formatValue("browser-languages-search")
+ );
+ menuitem.setAttribute("value", "search");
+ fragment.appendChild(menuitem);
+ }
+
+ let menulist = document.getElementById("primaryBrowserLocale");
+ let menupopup = menulist.querySelector("menupopup");
+ menupopup.textContent = "";
+ menupopup.appendChild(fragment);
+ menulist.value = selected;
+
+ document.getElementById("browserLanguagesBox").hidden = false;
+ },
+
+ /* Show the confirmation message bar to allow a restart into the new locales. */
+ async showConfirmLanguageChangeMessageBar(locales) {
+ let messageBar = document.getElementById("confirmBrowserLanguage");
+
+ // Get the bundle for the new locale.
+ let newBundle = getBundleForLocales(locales);
+
+ // Find the messages and labels.
+ let messages = await Promise.all(
+ [newBundle, document.l10n].map(async bundle =>
+ bundle.formatValue("confirm-browser-language-change-description")
+ )
+ );
+ let buttonLabels = await Promise.all(
+ [newBundle, document.l10n].map(async bundle =>
+ bundle.formatValue("confirm-browser-language-change-button")
+ )
+ );
+
+ // If both the message and label are the same, just include one row.
+ if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) {
+ messages.pop();
+ buttonLabels.pop();
+ }
+
+ let contentContainer = messageBar.querySelector(
+ ".message-bar-content-container"
+ );
+ contentContainer.textContent = "";
+
+ for (let i = 0; i < messages.length; i++) {
+ let messageContainer = document.createXULElement("hbox");
+ messageContainer.classList.add("message-bar-content");
+ messageContainer.style.flex = "1 50%";
+ messageContainer.setAttribute("align", "center");
+
+ let description = document.createXULElement("description");
+ description.classList.add("message-bar-description");
+
+ if (i == 0 && Services.intl.getScriptDirection(locales[0]) === "rtl") {
+ description.classList.add("rtl-locale");
+ }
+ description.setAttribute("flex", "1");
+ description.textContent = messages[i];
+ messageContainer.appendChild(description);
+
+ let button = document.createXULElement("button");
+ button.addEventListener(
+ "command",
+ gMainPane.confirmBrowserLanguageChange
+ );
+ button.classList.add("message-bar-button");
+ button.setAttribute("locales", locales.join(","));
+ button.setAttribute("label", buttonLabels[i]);
+ messageContainer.appendChild(button);
+
+ contentContainer.appendChild(messageContainer);
+ }
+
+ messageBar.hidden = false;
+ gMainPane.selectedLocalesForRestart = locales;
+ },
+
+ hideConfirmLanguageChangeMessageBar() {
+ let messageBar = document.getElementById("confirmBrowserLanguage");
+ messageBar.hidden = true;
+ let contentContainer = messageBar.querySelector(
+ ".message-bar-content-container"
+ );
+ contentContainer.textContent = "";
+ gMainPane.requestingLocales = null;
+ },
+
+ /* Confirm the locale change and restart the browser in the new locale. */
+ confirmBrowserLanguageChange(event) {
+ let localesString = (event.target.getAttribute("locales") || "").trim();
+ if (!localesString || !localesString.length) {
+ return;
+ }
+ let locales = localesString.split(",");
+ Services.locale.requestedLocales = locales;
+
+ // Record the change in telemetry before we restart.
+ gMainPane.recordBrowserLanguagesTelemetry("apply");
+
+ // Restart with the new locale.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (!cancelQuit.data) {
+ Services.startup.quit(
+ Services.startup.eAttemptQuit | Services.startup.eRestart
+ );
+ }
+ },
+
+ /* Show or hide the confirm change message bar based on the new locale. */
+ onPrimaryBrowserLanguageMenuChange(event) {
+ let locale = event.target.value;
+
+ if (locale == "search") {
+ gMainPane.showBrowserLanguagesSubDialog({ search: true });
+ return;
+ } else if (locale == Services.locale.appLocaleAsBCP47) {
+ this.hideConfirmLanguageChangeMessageBar();
+ return;
+ }
+
+ let newLocales = Array.from(
+ new Set([locale, ...Services.locale.requestedLocales]).values()
+ );
+
+ gMainPane.recordBrowserLanguagesTelemetry("reorder");
+
+ switch (gMainPane.getLanguageSwitchTransitionType(newLocales)) {
+ case "requires-restart":
+ // Prepare to change the locales, as they were different.
+ gMainPane.showConfirmLanguageChangeMessageBar(newLocales);
+ gMainPane.updatePrimaryBrowserLanguageUI(newLocales[0]);
+ break;
+ case "live-reload":
+ Services.locale.requestedLocales = newLocales;
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ case "locales-match":
+ // They matched, so we can reset the UI.
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ default:
+ throw new Error("Unhandled transition type.");
+ }
+ },
+
+ /**
+ * Takes as newZoom a floating point value representing the
+ * new default zoom. This value should not be a string, and
+ * should not carry a percentage sign/other localisation
+ * characteristics.
+ */
+ handleDefaultZoomChange(newZoom) {
+ let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+ );
+ let nonPrivateLoadContext = Cu.createLoadContext();
+ /* Because our setGlobal function takes in a browsing context, and
+ * because we want to keep this property consistent across both private
+ * and non-private contexts, we crate a non-private context and use that
+ * to set the property, regardless of our actual context.
+ */
+
+ let win = window.browsingContext.topChromeWindow;
+ cps2.setGlobal(win.FullZoom.name, newZoom, nonPrivateLoadContext);
+ },
+
+ onBrowserRestoreSessionChange(event) {
+ const value = event.target.checked;
+ const startupPref = Preferences.get("browser.startup.page");
+ let newValue;
+
+ if (value) {
+ // We need to restore the blank homepage setting in our other pref
+ if (startupPref.value === this.STARTUP_PREF_BLANK) {
+ HomePage.safeSet("about:blank");
+ }
+ newValue = this.STARTUP_PREF_RESTORE_SESSION;
+ } else {
+ newValue = this.STARTUP_PREF_HOMEPAGE;
+ }
+ startupPref.value = newValue;
+ },
+
+ // TABS
+
+ /*
+ * Preferences:
+ *
+ * browser.link.open_newwindow - int
+ * Determines where links targeting new windows should open.
+ * Values:
+ * 1 - Open in the current window or tab.
+ * 2 - Open in a new window.
+ * 3 - Open in a new tab in the most recent window.
+ * browser.tabs.loadInBackground - bool
+ * True - Whether browser should switch to a new tab opened from a link.
+ * browser.tabs.warnOnClose - bool
+ * True - If when closing a window with multiple tabs the user is warned and
+ * allowed to cancel the action, false to just close the window.
+ * browser.warnOnQuitShortcut - bool
+ * True - If the keyboard shortcut (Ctrl/Cmd+Q) is pressed, the user should
+ * be warned, false to just quit without prompting.
+ * browser.tabs.warnOnOpen - bool
+ * True - Whether the user should be warned when trying to open a lot of
+ * tabs at once (e.g. a large folder of bookmarks), allowing to
+ * cancel the action.
+ * browser.taskbar.previews.enable - bool
+ * True - Tabs are to be shown in Windows 7 taskbar.
+ * False - Only the window is to be shown in Windows 7 taskbar.
+ */
+
+ /**
+ * Determines where a link which opens a new window will open.
+ *
+ * @returns |true| if such links should be opened in new tabs
+ */
+ readLinkTarget() {
+ var openNewWindow = Preferences.get("browser.link.open_newwindow");
+ return openNewWindow.value != 2;
+ },
+
+ /**
+ * Determines where a link which opens a new window will open.
+ *
+ * @returns 2 if such links should be opened in new windows,
+ * 3 if such links should be opened in new tabs
+ */
+ writeLinkTarget() {
+ var linkTargeting = document.getElementById("linkTargeting");
+ return linkTargeting.checked ? 3 : 2;
+ },
+ /*
+ * Preferences:
+ *
+ * browser.shell.checkDefault
+ * - true if a default-browser check (and prompt to make it so if necessary)
+ * occurs at startup, false otherwise
+ */
+
+ /**
+ * Show button for setting browser as default browser or information that
+ * browser is already the default browser.
+ */
+ updateSetDefaultBrowser() {
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ let shellSvc = getShellService();
+ let defaultBrowserBox = document.getElementById("defaultBrowserBox");
+ let isInFlatpak = gGIOService?.isRunningUnderFlatpak;
+ // Flatpak does not support setting nor detection of default browser
+ if (!shellSvc || isInFlatpak) {
+ defaultBrowserBox.hidden = true;
+ return;
+ }
+ let isDefault = shellSvc.isDefaultBrowser(false, true);
+ let setDefaultPane = document.getElementById("setDefaultPane");
+ setDefaultPane.classList.toggle("is-default", isDefault);
+ let alwaysCheck = document.getElementById("alwaysCheckDefault");
+ let alwaysCheckPref = Preferences.get(
+ "browser.shell.checkDefaultBrowser"
+ );
+ alwaysCheck.disabled = alwaysCheckPref.locked || isDefault;
+ }
+ },
+
+ /**
+ * Set browser as the operating system default browser.
+ */
+ setDefaultBrowser() {
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ let alwaysCheckPref = Preferences.get(
+ "browser.shell.checkDefaultBrowser"
+ );
+ alwaysCheckPref.value = true;
+
+ // Reset exponential backoff delay time in order to do visual update in pollForDefaultBrowser.
+ this._backoffIndex = 0;
+
+ let shellSvc = getShellService();
+ if (!shellSvc) {
+ return;
+ }
+ try {
+ shellSvc.setDefaultBrowser(true, false);
+ } catch (ex) {
+ console.error(ex);
+ return;
+ }
+
+ let isDefault = shellSvc.isDefaultBrowser(false, true);
+ let setDefaultPane = document.getElementById("setDefaultPane");
+ setDefaultPane.classList.toggle("is-default", isDefault);
+ }
+ },
+
+ /**
+ * Shows a dialog in which the preferred language for web content may be set.
+ */
+ showLanguages() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/languages.xhtml"
+ );
+ },
+
+ recordBrowserLanguagesTelemetry(method, value = null) {
+ Services.telemetry.recordEvent(
+ "intl.ui.browserLanguage",
+ method,
+ "main",
+ value
+ );
+ },
+
+ /**
+ * Open the browser languages sub dialog in either the normal mode, or search mode.
+ * The search mode is only available from the menu to change the primary browser
+ * language.
+ *
+ * @param {{ search: boolean }}
+ */
+ showBrowserLanguagesSubDialog({ search }) {
+ // Record the telemetry event with an id to associate related actions.
+ let telemetryId = parseInt(
+ Services.telemetry.msSinceProcessStart(),
+ 10
+ ).toString();
+ let method = search ? "search" : "manage";
+ gMainPane.recordBrowserLanguagesTelemetry(method, telemetryId);
+
+ let opts = {
+ selectedLocalesForRestart: gMainPane.selectedLocalesForRestart,
+ search,
+ telemetryId,
+ };
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml",
+ { closingCallback: this.browserLanguagesClosed },
+ opts
+ );
+ },
+
+ /**
+ * Determine the transition strategy for switching the locale based on prefs
+ * and the switched locales.
+ *
+ * @param {Array<string>} newLocales - List of BCP 47 locale identifiers.
+ * @returns {"locales-match" | "requires-restart" | "live-reload"}
+ */
+ getLanguageSwitchTransitionType(newLocales) {
+ const { appLocalesAsBCP47 } = Services.locale;
+ if (appLocalesAsBCP47.join(",") === newLocales.join(",")) {
+ // The selected locales match, the order matters.
+ return "locales-match";
+ }
+
+ if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) {
+ if (
+ Services.intl.getScriptDirection(newLocales[0]) !==
+ Services.intl.getScriptDirection(appLocalesAsBCP47[0]) &&
+ !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional")
+ ) {
+ // Bug 1750852: The directionality of the text changed, which requires a restart
+ // until the quality of the switch can be improved.
+ return "requires-restart";
+ }
+
+ return "live-reload";
+ }
+
+ return "requires-restart";
+ },
+
+ /* Show or hide the confirm change message bar based on the updated ordering. */
+ browserLanguagesClosed() {
+ // When the subdialog is closed, settings are stored on gBrowserLanguagesDialog.
+ // The next time the dialog is opened, a new gBrowserLanguagesDialog is created.
+ let { selected } = this.gBrowserLanguagesDialog;
+
+ this.gBrowserLanguagesDialog.recordTelemetry(
+ selected ? "accept" : "cancel"
+ );
+
+ if (!selected) {
+ // No locales were selected. Cancel the operation.
+ return;
+ }
+
+ switch (gMainPane.getLanguageSwitchTransitionType(selected)) {
+ case "requires-restart":
+ gMainPane.showConfirmLanguageChangeMessageBar(selected);
+ gMainPane.updatePrimaryBrowserLanguageUI(selected[0]);
+ break;
+ case "live-reload":
+ Services.locale.requestedLocales = selected;
+
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ case "locales-match":
+ // They matched, so we can reset the UI.
+ gMainPane.updatePrimaryBrowserLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gMainPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ default:
+ throw new Error("Unhandled transition type.");
+ }
+ },
+
+ displayUseSystemLocale() {
+ let appLocale = Services.locale.appLocaleAsBCP47;
+ let regionalPrefsLocales = Services.locale.regionalPrefsLocales;
+ if (!regionalPrefsLocales.length) {
+ return;
+ }
+ let systemLocale = regionalPrefsLocales[0];
+ let localeDisplayname = Services.intl.getLocaleDisplayNames(
+ undefined,
+ [systemLocale],
+ { preferNative: true }
+ );
+ if (!localeDisplayname.length) {
+ return;
+ }
+ let localeName = localeDisplayname[0];
+ if (appLocale.split("-u-")[0] != systemLocale.split("-u-")[0]) {
+ let checkbox = document.getElementById("useSystemLocale");
+ document.l10n.setAttributes(checkbox, "use-system-locale", {
+ localeName,
+ });
+ checkbox.hidden = false;
+ }
+ },
+
+ /**
+ * Displays the translation exceptions dialog where specific site and language
+ * translation preferences can be set.
+ */
+ // TODO (Bug 1817084) Remove this code when we disable the extension
+ showTranslationExceptions() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml"
+ );
+ },
+
+ showTranslationsSettings() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/translations.xhtml"
+ );
+ },
+
+ openTranslationProviderAttribution() {
+ var { Translation } = ChromeUtils.import(
+ "resource:///modules/translation/TranslationParent.jsm"
+ );
+ Translation.openProviderAttribution();
+ },
+
+ /**
+ * Displays the fonts dialog, where web page font names and sizes can be
+ * configured.
+ */
+ configureFonts() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/fonts.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ /**
+ * Displays the colors dialog, where default web page/link/etc. colors can be
+ * configured.
+ */
+ configureColors() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/colors.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ // NETWORK
+ /**
+ * Displays a dialog in which proxy settings may be changed.
+ */
+ showConnections() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/connection.xhtml",
+ { closingCallback: this.updateProxySettingsUI.bind(this) }
+ );
+ },
+
+ // Update the UI to show the proper description depending on whether an
+ // extension is in control or not.
+ async updateProxySettingsUI() {
+ let controllingExtension = await getControllingExtension(
+ PREF_SETTING_TYPE,
+ PROXY_KEY
+ );
+ let description = document.getElementById("connectionSettingsDescription");
+
+ if (controllingExtension) {
+ setControllingExtensionDescription(
+ description,
+ controllingExtension,
+ "proxy.settings"
+ );
+ } else {
+ setControllingExtensionDescription(
+ description,
+ null,
+ "network-proxy-connection-description"
+ );
+ }
+ },
+
+ async checkBrowserContainers(event) {
+ let checkbox = document.getElementById("browserContainersCheckbox");
+ if (checkbox.checked) {
+ Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+ return;
+ }
+
+ let count = ContextualIdentityService.countContainerTabs();
+ if (count == 0) {
+ Services.prefs.setBoolPref("privacy.userContext.enabled", false);
+ return;
+ }
+
+ let [title, message, okButton, cancelButton] =
+ await document.l10n.formatValues([
+ { id: "containers-disable-alert-title" },
+ { id: "containers-disable-alert-desc", args: { tabCount: count } },
+ { id: "containers-disable-alert-ok-button", args: { tabCount: count } },
+ { id: "containers-disable-alert-cancel-button" },
+ ]);
+
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1;
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ okButton,
+ cancelButton,
+ null,
+ null,
+ {}
+ );
+ if (rv == 0) {
+ Services.prefs.setBoolPref("privacy.userContext.enabled", false);
+ return;
+ }
+
+ checkbox.checked = true;
+ },
+
+ /**
+ * Displays container panel for customising and adding containers.
+ */
+ showContainerSettings() {
+ gotoPref("containers");
+ },
+
+ /**
+ * ui.osk.enabled
+ * - when set to true, subject to other conditions, we may sometimes invoke
+ * an on-screen keyboard when a text input is focused.
+ * (Currently Windows-only, and depending on prefs, may be Windows-8-only)
+ */
+ updateOnScreenKeyboardVisibility() {
+ if (AppConstants.platform == "win") {
+ let minVersion = Services.prefs.getBoolPref("ui.osk.require_win10")
+ ? 10
+ : 6.2;
+ if (
+ Services.vc.compare(
+ Services.sysinfo.getProperty("version"),
+ minVersion
+ ) >= 0
+ ) {
+ document.getElementById("useOnScreenKeyboard").hidden = false;
+ }
+ }
+ },
+
+ updateHardwareAcceleration() {
+ // Placeholder for restart on change
+ },
+
+ // FONTS
+
+ /**
+ * Populates the default font list in UI.
+ */
+ _rebuildFonts() {
+ var langGroupPref = Preferences.get("font.language.group");
+ var isSerif =
+ this._readDefaultFontTypeForLanguage(langGroupPref.value) == "serif";
+ this._selectDefaultLanguageGroup(langGroupPref.value, isSerif);
+ },
+
+ /**
+ * Returns the type of the current default font for the language denoted by
+ * aLanguageGroup.
+ */
+ _readDefaultFontTypeForLanguage(aLanguageGroup) {
+ const kDefaultFontType = "font.default.%LANG%";
+ var defaultFontTypePref = kDefaultFontType.replace(
+ /%LANG%/,
+ aLanguageGroup
+ );
+ var preference = Preferences.get(defaultFontTypePref);
+ if (!preference) {
+ preference = Preferences.add({ id: defaultFontTypePref, type: "string" });
+ preference.on("change", gMainPane._rebuildFonts.bind(gMainPane));
+ }
+ return preference.value;
+ },
+
+ _selectDefaultLanguageGroupPromise: Promise.resolve(),
+
+ _selectDefaultLanguageGroup(aLanguageGroup, aIsSerif) {
+ this._selectDefaultLanguageGroupPromise = (async () => {
+ // Avoid overlapping language group selections by awaiting the resolution
+ // of the previous one. We do this because this function is re-entrant,
+ // as inserting <preference> elements into the DOM sometimes triggers a call
+ // back into this function. And since this function is also asynchronous,
+ // that call can enter this function before the previous run has completed,
+ // which would corrupt the font menulists. Awaiting the previous call's
+ // resolution avoids that fate.
+ await this._selectDefaultLanguageGroupPromise;
+
+ const kFontNameFmtSerif = "font.name.serif.%LANG%";
+ const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
+ const kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
+ const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
+ const kFontSizeFmtVariable = "font.size.variable.%LANG%";
+
+ var prefs = [
+ {
+ format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif,
+ type: "fontname",
+ element: "defaultFont",
+ fonttype: aIsSerif ? "serif" : "sans-serif",
+ },
+ {
+ format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif,
+ type: "unichar",
+ element: null,
+ fonttype: aIsSerif ? "serif" : "sans-serif",
+ },
+ {
+ format: kFontSizeFmtVariable,
+ type: "int",
+ element: "defaultFontSize",
+ fonttype: null,
+ },
+ ];
+ for (var i = 0; i < prefs.length; ++i) {
+ var preference = Preferences.get(
+ prefs[i].format.replace(/%LANG%/, aLanguageGroup)
+ );
+ if (!preference) {
+ var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup);
+ preference = Preferences.add({ id: name, type: prefs[i].type });
+ }
+
+ if (!prefs[i].element) {
+ continue;
+ }
+
+ var element = document.getElementById(prefs[i].element);
+ if (element) {
+ element.setAttribute("preference", preference.id);
+
+ if (prefs[i].fonttype) {
+ await FontBuilder.buildFontList(
+ aLanguageGroup,
+ prefs[i].fonttype,
+ element
+ );
+ }
+
+ preference.setElementValue(element);
+ }
+ }
+ })().catch(console.error);
+ },
+
+ onMigrationButtonCommand(command) {
+ // When browser.migrate.content-modal.enabled is enabled by default,
+ // the event handler can just call showMigrationWizardDialog directly,
+ // but for now, we delegate to MigrationUtils to open the native modal
+ // in case that's the dialog we're still using.
+ //
+ // Enabling the pref by default will be part of bug 1822156.
+ const browser = window.docShell.chromeEventHandler;
+ const browserWindow = browser.ownerGlobal;
+
+ // showMigrationWizard blocks on some platforms. We'll dispatch the request
+ // to open to a runnable on the main thread so that we don't have to block
+ // this function call.
+ Services.tm.dispatchToMainThread(() => {
+ MigrationUtils.showMigrationWizard(browserWindow, {
+ entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES,
+ });
+ });
+ },
+
+ /**
+ * Displays the migration wizard dialog in an HTML dialog.
+ */
+ async showMigrationWizardDialog({ closeTabWhenDone = false } = {}) {
+ let migrationWizardDialog = document.getElementById(
+ "migrationWizardDialog"
+ );
+
+ if (migrationWizardDialog.open) {
+ return;
+ }
+
+ await customElements.whenDefined("migration-wizard");
+
+ // If we've been opened before, remove the old wizard and insert a
+ // new one to put it back into its starting state.
+ if (!migrationWizardDialog.firstElementChild) {
+ let wizard = document.createElement("migration-wizard");
+ wizard.toggleAttribute("dialog-mode", true);
+
+ let panelList = document.createElement("panel-list");
+ let panel = document.createXULElement("panel");
+ panel.appendChild(panelList);
+ wizard.appendChild(panel);
+
+ migrationWizardDialog.appendChild(wizard);
+ }
+ migrationWizardDialog.firstElementChild.requestState();
+
+ migrationWizardDialog.addEventListener(
+ "close",
+ () => {
+ // Let others know that the wizard is closed -- potentially because of a
+ // user action within the dialog that dispatches "MigrationWizard:Close"
+ // but this also covers cases like hitting Escape.
+ Services.obs.notifyObservers(
+ migrationWizardDialog,
+ "MigrationWizard:Closed"
+ );
+ if (closeTabWhenDone) {
+ window.close();
+ }
+ },
+ { once: true }
+ );
+
+ migrationWizardDialog.showModal();
+ },
+
+ /**
+ * Stores the original value of the spellchecking preference to enable proper
+ * restoration if unchanged (since we're mapping a tristate onto a checkbox).
+ */
+ _storedSpellCheck: 0,
+
+ /**
+ * Returns true if any spellchecking is enabled and false otherwise, caching
+ * the current value to enable proper pref restoration if the checkbox is
+ * never changed.
+ *
+ * layout.spellcheckDefault
+ * - an integer:
+ * 0 disables spellchecking
+ * 1 enables spellchecking, but only for multiline text fields
+ * 2 enables spellchecking for all text fields
+ */
+ readCheckSpelling() {
+ var pref = Preferences.get("layout.spellcheckDefault");
+ this._storedSpellCheck = pref.value;
+
+ return pref.value != 0;
+ },
+
+ /**
+ * Returns the value of the spellchecking preference represented by UI,
+ * preserving the preference's "hidden" value if the preference is
+ * unchanged and represents a value not strictly allowed in UI.
+ */
+ writeCheckSpelling() {
+ var checkbox = document.getElementById("checkSpelling");
+ if (checkbox.checked) {
+ if (this._storedSpellCheck == 2) {
+ return 2;
+ }
+ return 1;
+ }
+ return 0;
+ },
+
+ updateDefaultPerformanceSettingsPref() {
+ let defaultPerformancePref = Preferences.get(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ );
+ let processCountPref = Preferences.get("dom.ipc.processCount");
+ let accelerationPref = Preferences.get("layers.acceleration.disabled");
+ if (
+ processCountPref.value != processCountPref.defaultValue ||
+ accelerationPref.value != accelerationPref.defaultValue
+ ) {
+ defaultPerformancePref.value = false;
+ }
+ },
+
+ updatePerformanceSettingsBox({ duringChangeEvent }) {
+ let defaultPerformancePref = Preferences.get(
+ "browser.preferences.defaultPerformanceSettings.enabled"
+ );
+ let performanceSettings = document.getElementById("performanceSettings");
+ let processCountPref = Preferences.get("dom.ipc.processCount");
+ if (defaultPerformancePref.value) {
+ let accelerationPref = Preferences.get("layers.acceleration.disabled");
+ // Unset the value so process count will be decided by the platform.
+ processCountPref.value = processCountPref.defaultValue;
+ accelerationPref.value = accelerationPref.defaultValue;
+ performanceSettings.hidden = true;
+ } else {
+ performanceSettings.hidden = false;
+ }
+ },
+
+ buildContentProcessCountMenuList() {
+ if (Services.appinfo.fissionAutostart) {
+ document.getElementById("limitContentProcess").hidden = true;
+ document.getElementById("contentProcessCount").hidden = true;
+ document.getElementById(
+ "contentProcessCountEnabledDescription"
+ ).hidden = true;
+ document.getElementById(
+ "contentProcessCountDisabledDescription"
+ ).hidden = true;
+ return;
+ }
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ let processCountPref = Preferences.get("dom.ipc.processCount");
+ let defaultProcessCount = processCountPref.defaultValue;
+
+ let contentProcessCount =
+ document.querySelector(`#contentProcessCount > menupopup >
+ menuitem[value="${defaultProcessCount}"]`);
+
+ document.l10n.setAttributes(
+ contentProcessCount,
+ "performance-default-content-process-count",
+ { num: defaultProcessCount }
+ );
+
+ document.getElementById("limitContentProcess").disabled = false;
+ document.getElementById("contentProcessCount").disabled = false;
+ document.getElementById(
+ "contentProcessCountEnabledDescription"
+ ).hidden = false;
+ document.getElementById(
+ "contentProcessCountDisabledDescription"
+ ).hidden = true;
+ } else {
+ document.getElementById("limitContentProcess").disabled = true;
+ document.getElementById("contentProcessCount").disabled = true;
+ document.getElementById(
+ "contentProcessCountEnabledDescription"
+ ).hidden = true;
+ document.getElementById(
+ "contentProcessCountDisabledDescription"
+ ).hidden = false;
+ }
+ },
+
+ _minUpdatePrefDisableTime: 1000,
+ /**
+ * Selects the correct item in the update radio group
+ */
+ async readUpdateAutoPref() {
+ if (
+ AppConstants.MOZ_UPDATER &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp
+ ) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+
+ radiogroup.disabled = true;
+ let enabled = await UpdateUtils.getAppUpdateAutoEnabled();
+ radiogroup.value = enabled;
+ radiogroup.disabled = false;
+
+ this.maybeDisableBackgroundUpdateControls();
+ }
+ },
+
+ /**
+ * Writes the value of the automatic update radio group to the disk
+ */
+ async writeUpdateAutoPref() {
+ if (
+ AppConstants.MOZ_UPDATER &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp
+ ) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+ let updateAutoValue = radiogroup.value == "true";
+ let _disableTimeOverPromise = new Promise(r =>
+ setTimeout(r, this._minUpdatePrefDisableTime)
+ );
+ radiogroup.disabled = true;
+ try {
+ await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue);
+ await _disableTimeOverPromise;
+ radiogroup.disabled = false;
+ } catch (error) {
+ console.error(error);
+ await Promise.all([
+ this.readUpdateAutoPref(),
+ this.reportUpdatePrefWriteError(),
+ ]);
+ return;
+ }
+
+ this.maybeDisableBackgroundUpdateControls();
+
+ // If the value was changed to false the user should be given the option
+ // to discard an update if there is one.
+ if (!updateAutoValue) {
+ await this.checkUpdateInProgress();
+ }
+ // For tests:
+ radiogroup.dispatchEvent(new CustomEvent("ProcessedUpdatePrefChange"));
+ }
+ },
+
+ isBackgroundUpdateUIAvailable() {
+ return (
+ AppConstants.MOZ_UPDATE_AGENT &&
+ // This UI controls a per-installation pref. It won't necessarily work
+ // properly if per-installation prefs aren't supported.
+ UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp &&
+ !UpdateUtils.appUpdateSettingIsLocked("app.update.background.enabled")
+ );
+ },
+
+ maybeDisableBackgroundUpdateControls() {
+ if (this.isBackgroundUpdateUIAvailable()) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+ let updateAutoEnabled = radiogroup.value == "true";
+
+ // This control is only active if auto update is enabled.
+ document.getElementById("backgroundUpdate").disabled = !updateAutoEnabled;
+ }
+ },
+
+ async readBackgroundUpdatePref() {
+ const prefName = "app.update.background.enabled";
+ if (this.isBackgroundUpdateUIAvailable()) {
+ let backgroundCheckbox = document.getElementById("backgroundUpdate");
+
+ // When the page first loads, the checkbox is unchecked until we finish
+ // reading the config file from the disk. But, ideally, we don't want to
+ // give the user the impression that this setting has somehow gotten
+ // turned off and they need to turn it back on. We also don't want the
+ // user interacting with the control, expecting a particular behavior, and
+ // then have the read complete and change the control in an unexpected
+ // way. So we disable the control while we are reading.
+ // The only entry points for this function are page load and user
+ // interaction with the control. By disabling the control to prevent
+ // further user interaction, we prevent the possibility of entering this
+ // function a second time while we are still reading.
+ backgroundCheckbox.disabled = true;
+
+ // If we haven't already done this, it might result in the effective value
+ // of the Background Update pref changing. Thus, we should do it before
+ // we tell the user what value this pref has.
+ await BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed();
+
+ let enabled = await UpdateUtils.readUpdateConfigSetting(prefName);
+ backgroundCheckbox.checked = enabled;
+ this.maybeDisableBackgroundUpdateControls();
+ }
+ },
+
+ async writeBackgroundUpdatePref() {
+ const prefName = "app.update.background.enabled";
+ if (this.isBackgroundUpdateUIAvailable()) {
+ let backgroundCheckbox = document.getElementById("backgroundUpdate");
+ backgroundCheckbox.disabled = true;
+ let backgroundUpdateEnabled = backgroundCheckbox.checked;
+ try {
+ await UpdateUtils.writeUpdateConfigSetting(
+ prefName,
+ backgroundUpdateEnabled
+ );
+ } catch (error) {
+ console.error(error);
+ await this.readBackgroundUpdatePref();
+ await this.reportUpdatePrefWriteError();
+ return;
+ }
+
+ this.maybeDisableBackgroundUpdateControls();
+ }
+ },
+
+ async reportUpdatePrefWriteError() {
+ let [title, message] = await document.l10n.formatValues([
+ { id: "update-setting-write-failure-title2" },
+ {
+ id: "update-setting-write-failure-message2",
+ args: { path: UpdateUtils.configFilePath },
+ },
+ ]);
+
+ // Set up the Ok Button
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK;
+ Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+ },
+
+ async checkUpdateInProgress() {
+ let um = Cc["@mozilla.org/updates/update-manager;1"].getService(
+ Ci.nsIUpdateManager
+ );
+ if (!um.readyUpdate && !um.downloadingUpdate) {
+ return;
+ }
+
+ let [title, message, okButton, cancelButton] =
+ await document.l10n.formatValues([
+ { id: "update-in-progress-title" },
+ { id: "update-in-progress-message" },
+ { id: "update-in-progress-ok-button" },
+ { id: "update-in-progress-cancel-button" },
+ ]);
+
+ // Continue is the cancel button which is BUTTON_POS_1 and is set as the
+ // default so pressing escape or using a platform standard method of closing
+ // the UI will not discard the update.
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 +
+ Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ okButton,
+ cancelButton,
+ null,
+ null,
+ {}
+ );
+ if (rv != 1) {
+ let aus = Cc["@mozilla.org/updates/update-service;1"].getService(
+ Ci.nsIApplicationUpdateService
+ );
+ await aus.stopDownload();
+ um.cleanupReadyUpdate();
+ um.cleanupDownloadingUpdate();
+ }
+ },
+
+ /**
+ * Displays the history of installed updates.
+ */
+ showUpdates() {
+ gSubDialog.open("chrome://mozapps/content/update/history.xhtml");
+ },
+
+ destroy() {
+ window.removeEventListener("unload", this);
+ Services.prefs.removeObserver(PREF_CONTAINERS_EXTENSION, this);
+ Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
+ Services.obs.removeObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
+ AppearanceChooser.destroy();
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ // nsIObserver
+
+ async observe(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed") {
+ if (aData == PREF_CONTAINERS_EXTENSION) {
+ this.readBrowserContainersCheckbox();
+ return;
+ }
+ // Rebuild the list when there are changes to preferences that influence
+ // whether or not to show certain entries in the list.
+ if (!this._storingAction) {
+ await this._rebuildView();
+ }
+ } else if (aTopic == AUTO_UPDATE_CHANGED_TOPIC) {
+ if (!AppConstants.MOZ_UPDATER) {
+ return;
+ }
+ if (aData != "true" && aData != "false") {
+ throw new Error("Invalid preference value for app.update.auto");
+ }
+ document.getElementById("updateRadioGroup").value = aData;
+ this.maybeDisableBackgroundUpdateControls();
+ } else if (aTopic == BACKGROUND_UPDATE_CHANGED_TOPIC) {
+ if (!AppConstants.MOZ_UPDATE_AGENT) {
+ return;
+ }
+ if (aData != "true" && aData != "false") {
+ throw new Error(
+ "Invalid preference value for app.update.background.enabled"
+ );
+ }
+ document.getElementById("backgroundUpdate").checked = aData == "true";
+ }
+ },
+
+ // EventListener
+
+ handleEvent(aEvent) {
+ if (aEvent.type == "unload") {
+ this.destroy();
+ if (AppConstants.MOZ_UPDATER) {
+ onUnload();
+ }
+ }
+ },
+
+ // Composed Model Construction
+
+ _loadData() {
+ this._loadInternalHandlers();
+ this._loadApplicationHandlers();
+ },
+
+ /**
+ * Load higher level internal handlers so they can be turned on/off in the
+ * applications menu.
+ */
+ _loadInternalHandlers() {
+ let internalHandlers = [new PDFHandlerInfoWrapper()];
+
+ let enabledHandlers = Services.prefs
+ .getCharPref("browser.download.viewableInternally.enabledTypes", "")
+ .trim();
+ if (enabledHandlers) {
+ for (let ext of enabledHandlers.split(",")) {
+ internalHandlers.push(
+ new ViewableInternallyHandlerInfoWrapper(null, ext.trim())
+ );
+ }
+ }
+ for (let internalHandler of internalHandlers) {
+ if (internalHandler.enabled) {
+ this._handledTypes[internalHandler.type] = internalHandler;
+ }
+ }
+ },
+
+ /**
+ * Load the set of handlers defined by the application datastore.
+ */
+ _loadApplicationHandlers() {
+ for (let wrappedHandlerInfo of gHandlerService.enumerate()) {
+ let type = wrappedHandlerInfo.type;
+
+ let handlerInfoWrapper;
+ if (type in this._handledTypes) {
+ handlerInfoWrapper = this._handledTypes[type];
+ } else {
+ if (DownloadIntegration.shouldViewDownloadInternally(type)) {
+ handlerInfoWrapper = new ViewableInternallyHandlerInfoWrapper(type);
+ } else {
+ handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo);
+ }
+ this._handledTypes[type] = handlerInfoWrapper;
+ }
+ }
+ },
+
+ // View Construction
+
+ selectedHandlerListItem: null,
+
+ _initListEventHandlers() {
+ this._list.addEventListener("select", event => {
+ if (event.target != this._list) {
+ return;
+ }
+
+ let handlerListItem =
+ this._list.selectedItem &&
+ HandlerListItem.forNode(this._list.selectedItem);
+ if (this.selectedHandlerListItem == handlerListItem) {
+ return;
+ }
+
+ if (this.selectedHandlerListItem) {
+ this.selectedHandlerListItem.showActionsMenu = false;
+ }
+ this.selectedHandlerListItem = handlerListItem;
+ if (handlerListItem) {
+ this.rebuildActionsMenu();
+ handlerListItem.showActionsMenu = true;
+ }
+ });
+ },
+
+ async _rebuildVisibleTypes() {
+ this._visibleTypes = [];
+
+ // Map whose keys are string descriptions and values are references to the
+ // first visible HandlerInfoWrapper that has this description. We use this
+ // to determine whether or not to annotate descriptions with their types to
+ // distinguish duplicate descriptions from each other.
+ let visibleDescriptions = new Map();
+ for (let type in this._handledTypes) {
+ // Yield before processing each handler info object to avoid monopolizing
+ // the main thread, as the objects are retrieved lazily, and retrieval
+ // can be expensive on Windows.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ let handlerInfo = this._handledTypes[type];
+
+ // We couldn't find any reason to exclude the type, so include it.
+ this._visibleTypes.push(handlerInfo);
+
+ let key = JSON.stringify(handlerInfo.description);
+ let otherHandlerInfo = visibleDescriptions.get(key);
+ if (!otherHandlerInfo) {
+ // This is the first type with this description that we encountered
+ // while rebuilding the _visibleTypes array this time. Make sure the
+ // flag is reset so we won't add the type to the description.
+ handlerInfo.disambiguateDescription = false;
+ visibleDescriptions.set(key, handlerInfo);
+ } else {
+ // There is at least another type with this description. Make sure we
+ // add the type to the description on both HandlerInfoWrapper objects.
+ handlerInfo.disambiguateDescription = true;
+ otherHandlerInfo.disambiguateDescription = true;
+ }
+ }
+ },
+
+ async _rebuildView() {
+ let lastSelectedType =
+ this.selectedHandlerListItem &&
+ this.selectedHandlerListItem.handlerInfoWrapper.type;
+ this.selectedHandlerListItem = null;
+
+ // Clear the list of entries.
+ this._list.textContent = "";
+
+ var visibleTypes = this._visibleTypes;
+
+ let items = visibleTypes.map(
+ visibleType => new HandlerListItem(visibleType)
+ );
+ let itemsFragment = document.createDocumentFragment();
+ let lastSelectedItem;
+ for (let item of items) {
+ item.createNode(itemsFragment);
+ if (item.handlerInfoWrapper.type == lastSelectedType) {
+ lastSelectedItem = item;
+ }
+ }
+
+ for (let item of items) {
+ item.setupNode();
+ this.rebuildActionsMenu(item.node, item.handlerInfoWrapper);
+ item.refreshAction();
+ }
+
+ // If the user is filtering the list, then only show matching types.
+ // If we filter, we need to first localize the fragment, to
+ // be able to filter by localized values.
+ if (this._filter.value) {
+ await document.l10n.translateFragment(itemsFragment);
+
+ this._filterView(itemsFragment);
+
+ document.l10n.pauseObserving();
+ this._list.appendChild(itemsFragment);
+ document.l10n.resumeObserving();
+ } else {
+ // Otherwise we can just append the fragment and it'll
+ // get localized via the Mutation Observer.
+ this._list.appendChild(itemsFragment);
+ }
+
+ if (lastSelectedItem) {
+ this._list.selectedItem = lastSelectedItem.node;
+ }
+ },
+
+ /**
+ * Whether or not the given handler app is valid.
+ *
+ * @param aHandlerApp {nsIHandlerApp} the handler app in question
+ *
+ * @returns {boolean} whether or not it's valid
+ */
+ isValidHandlerApp(aHandlerApp) {
+ if (!aHandlerApp) {
+ return false;
+ }
+
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ return this._isValidHandlerExecutable(aHandlerApp.executable);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
+ return aHandlerApp.uriTemplate;
+ }
+
+ if (aHandlerApp instanceof Ci.nsIGIOMimeApp) {
+ return aHandlerApp.command;
+ }
+
+ return false;
+ },
+
+ _isValidHandlerExecutable(aExecutable) {
+ let leafName;
+ if (AppConstants.platform == "win") {
+ leafName = `${AppConstants.MOZ_APP_NAME}.exe`;
+ } else if (AppConstants.platform == "macosx") {
+ leafName = AppConstants.MOZ_MACBUNDLE_NAME;
+ } else {
+ leafName = `${AppConstants.MOZ_APP_NAME}-bin`;
+ }
+ return (
+ aExecutable &&
+ aExecutable.exists() &&
+ aExecutable.isExecutable() &&
+ // XXXben - we need to compare this with the running instance executable
+ // just don't know how to do that via script...
+ // XXXmano TBD: can probably add this to nsIShellService
+ aExecutable.leafName != leafName
+ );
+ },
+
+ /**
+ * Rebuild the actions menu for the selected entry. Gets called by
+ * the richlistitem constructor when an entry in the list gets selected.
+ */
+ rebuildActionsMenu(
+ typeItem = this._list.selectedItem,
+ handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper
+ ) {
+ var menu = typeItem.querySelector(".actionsMenu");
+ var menuPopup = menu.menupopup;
+
+ // Clear out existing items.
+ while (menuPopup.hasChildNodes()) {
+ menuPopup.removeChild(menuPopup.lastChild);
+ }
+
+ let internalMenuItem;
+ // Add the "Open in Firefox" option for optional internal handlers.
+ if (
+ handlerInfo instanceof InternalHandlerInfoWrapper &&
+ !handlerInfo.preventInternalViewing
+ ) {
+ internalMenuItem = document.createXULElement("menuitem");
+ internalMenuItem.setAttribute(
+ "action",
+ Ci.nsIHandlerInfo.handleInternally
+ );
+ document.l10n.setAttributes(internalMenuItem, "applications-open-inapp");
+ internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "handleInternally");
+ menuPopup.appendChild(internalMenuItem);
+ }
+
+ var askMenuItem = document.createXULElement("menuitem");
+ askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk);
+ document.l10n.setAttributes(askMenuItem, "applications-always-ask");
+ askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask");
+ menuPopup.appendChild(askMenuItem);
+
+ // Create a menu item for saving to disk.
+ // Note: this option isn't available to protocol types, since we don't know
+ // what it means to save a URL having a certain scheme to disk.
+ if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ var saveMenuItem = document.createXULElement("menuitem");
+ saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk);
+ document.l10n.setAttributes(saveMenuItem, "applications-action-save");
+ saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save");
+ menuPopup.appendChild(saveMenuItem);
+ }
+
+ // Add a separator to distinguish these items from the helper app items
+ // that follow them.
+ let menuseparator = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuseparator);
+
+ // Create a menu item for the OS default application, if any.
+ if (handlerInfo.hasDefaultHandler) {
+ var defaultMenuItem = document.createXULElement("menuitem");
+ defaultMenuItem.setAttribute(
+ "action",
+ Ci.nsIHandlerInfo.useSystemDefault
+ );
+ // If an internal option is available, don't show the application
+ // name for the OS default to prevent two options from appearing
+ // that may both say "Firefox".
+ if (internalMenuItem) {
+ document.l10n.setAttributes(
+ defaultMenuItem,
+ "applications-use-os-default"
+ );
+ defaultMenuItem.setAttribute("image", ICON_URL_APP);
+ } else {
+ document.l10n.setAttributes(
+ defaultMenuItem,
+ "applications-use-app-default",
+ {
+ "app-name": handlerInfo.defaultDescription,
+ }
+ );
+ defaultMenuItem.setAttribute(
+ "image",
+ handlerInfo.iconURLForSystemDefault
+ );
+ }
+
+ menuPopup.appendChild(defaultMenuItem);
+ }
+
+ // Create menu items for possible handlers.
+ let preferredApp = handlerInfo.preferredApplicationHandler;
+ var possibleAppMenuItems = [];
+ for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) {
+ if (!this.isValidHandlerApp(possibleApp)) {
+ continue;
+ }
+
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
+ let label;
+ if (possibleApp instanceof Ci.nsILocalHandlerApp) {
+ label = getFileDisplayName(possibleApp.executable);
+ } else {
+ label = possibleApp.name;
+ }
+ document.l10n.setAttributes(menuItem, "applications-use-app", {
+ "app-name": label,
+ });
+ menuItem.setAttribute(
+ "image",
+ this._getIconURLForHandlerApp(possibleApp)
+ );
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = possibleApp;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+ // Add gio handlers
+ if (gGIOService) {
+ var gioApps = gGIOService.getAppsForURIScheme(handlerInfo.type);
+ let possibleHandlers = handlerInfo.possibleApplicationHandlers;
+ for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) {
+ // OS handler share the same name, it's most likely the same app, skipping...
+ if (handler.name == handlerInfo.defaultDescription) {
+ continue;
+ }
+ // Check if the handler is already in possibleHandlers
+ let appAlreadyInHandlers = false;
+ for (let i = possibleHandlers.length - 1; i >= 0; --i) {
+ let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+ // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp
+ if (handler.equals(app)) {
+ appAlreadyInHandlers = true;
+ break;
+ }
+ }
+ if (!appAlreadyInHandlers) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
+ document.l10n.setAttributes(menuItem, "applications-use-app", {
+ "app-name": handler.name,
+ });
+ menuItem.setAttribute(
+ "image",
+ this._getIconURLForHandlerApp(handler)
+ );
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = handler;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+ }
+ }
+
+ // Create a menu item for selecting a local application.
+ let canOpenWithOtherApp = true;
+ if (AppConstants.platform == "win") {
+ // On Windows, selecting an application to open another application
+ // would be meaningless so we special case executables.
+ let executableType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromExtension("exe");
+ canOpenWithOtherApp = handlerInfo.type != executableType;
+ }
+ if (canOpenWithOtherApp) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.className = "choose-app-item";
+ menuItem.addEventListener("command", function (e) {
+ gMainPane.chooseApp(e);
+ });
+ document.l10n.setAttributes(menuItem, "applications-use-other");
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Create a menu item for managing applications.
+ if (possibleAppMenuItems.length) {
+ let menuItem = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuItem);
+ menuItem = document.createXULElement("menuitem");
+ menuItem.className = "manage-app-item";
+ menuItem.addEventListener("command", function (e) {
+ gMainPane.manageApp(e);
+ });
+ document.l10n.setAttributes(menuItem, "applications-manage-app");
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Select the item corresponding to the preferred action. If the always
+ // ask flag is set, it overrides the preferred action. Otherwise we pick
+ // the item identified by the preferred action (when the preferred action
+ // is to use a helper app, we have to pick the specific helper app item).
+ if (handlerInfo.alwaysAskBeforeHandling) {
+ menu.selectedItem = askMenuItem;
+ } else {
+ // The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
+ // the actions the application can take with content of various types.
+ // But since we've stopped support for plugins, there's no value
+ // identifying the "use plugin" action, so we use this constant instead.
+ const kActionUsePlugin = 5;
+
+ switch (handlerInfo.preferredAction) {
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (internalMenuItem) {
+ menu.selectedItem = internalMenuItem;
+ } else {
+ console.error("No menu item defined to set!");
+ }
+ break;
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ // We might not have a default item if we're not aware of an
+ // OS-default handler for this type:
+ menu.selectedItem = defaultMenuItem || askMenuItem;
+ break;
+ case Ci.nsIHandlerInfo.useHelperApp:
+ if (preferredApp) {
+ let preferredItem = possibleAppMenuItems.find(v =>
+ v.handlerApp.equals(preferredApp)
+ );
+ if (preferredItem) {
+ menu.selectedItem = preferredItem;
+ } else {
+ // This shouldn't happen, but let's make sure we end up with a
+ // selected item:
+ let possible = possibleAppMenuItems
+ .map(v => v.handlerApp && v.handlerApp.name)
+ .join(", ");
+ console.error(
+ new Error(
+ `Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})`
+ )
+ );
+ menu.selectedItem = askMenuItem;
+ }
+ }
+ break;
+ case kActionUsePlugin:
+ // We no longer support plugins, select "ask" instead:
+ menu.selectedItem = askMenuItem;
+ break;
+ case Ci.nsIHandlerInfo.saveToDisk:
+ menu.selectedItem = saveMenuItem;
+ break;
+ }
+ }
+ },
+
+ // Sorting & Filtering
+
+ _sortColumn: null,
+
+ /**
+ * Sort the list when the user clicks on a column header.
+ */
+ sort(event) {
+ if (event.button != 0) {
+ return;
+ }
+ var column = event.target;
+
+ // If the user clicked on a new sort column, remove the direction indicator
+ // from the old column.
+ if (this._sortColumn && this._sortColumn != column) {
+ this._sortColumn.removeAttribute("sortDirection");
+ }
+
+ this._sortColumn = column;
+
+ // Set (or switch) the sort direction indicator.
+ if (column.getAttribute("sortDirection") == "ascending") {
+ column.setAttribute("sortDirection", "descending");
+ } else {
+ column.setAttribute("sortDirection", "ascending");
+ }
+
+ this._sortListView();
+ },
+
+ async _sortListView() {
+ if (!this._sortColumn) {
+ return;
+ }
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ await document.l10n.translateFragment(this._list);
+ let items = Array.from(this._list.children);
+
+ let textForNode;
+ if (this._sortColumn.getAttribute("value") === "type") {
+ textForNode = n => n.querySelector(".typeDescription").textContent;
+ } else {
+ textForNode = n => n.querySelector(".actionsMenu").getAttribute("label");
+ }
+
+ let sortDir = this._sortColumn.getAttribute("sortDirection");
+ let multiplier = sortDir == "descending" ? -1 : 1;
+ items.sort(
+ (a, b) => multiplier * comp.compare(textForNode(a), textForNode(b))
+ );
+
+ // Re-append items in the correct order:
+ items.forEach(item => this._list.appendChild(item));
+ },
+
+ _filterView(frag = this._list) {
+ const filterValue = this._filter.value.toLowerCase();
+ for (let elem of frag.children) {
+ const typeDescription =
+ elem.querySelector(".typeDescription").textContent;
+ const actionDescription = elem
+ .querySelector(".actionDescription")
+ .getAttribute("value");
+ elem.hidden =
+ !typeDescription.toLowerCase().includes(filterValue) &&
+ !actionDescription.toLowerCase().includes(filterValue);
+ }
+ },
+
+ /**
+ * Filter the list when the user enters a filter term into the filter field.
+ */
+ filter() {
+ this._rebuildView(); // FIXME: Should this be await since bug 1508156?
+ },
+
+ focusFilterBox() {
+ this._filter.focus();
+ this._filter.select();
+ },
+
+ // Changes
+
+ // Whether or not we are currently storing the action selected by the user.
+ // We use this to suppress notification-triggered updates to the list when
+ // we make changes that may spawn such updates.
+ // XXXgijs: this was definitely necessary when we changed feed preferences
+ // from within _storeAction and its calltree. Now, it may still be
+ // necessary, to avoid calling _rebuildView. bug 1499350 has more details.
+ _storingAction: false,
+
+ onSelectAction(aActionItem) {
+ this._storingAction = true;
+
+ try {
+ this._storeAction(aActionItem);
+ } finally {
+ this._storingAction = false;
+ }
+ },
+
+ _storeAction(aActionItem) {
+ var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
+
+ let action = parseInt(aActionItem.getAttribute("action"));
+
+ // Set the preferred application handler.
+ // We leave the existing preferred app in the list when we set
+ // the preferred action to something other than useHelperApp so that
+ // legacy datastores that don't have the preferred app in the list
+ // of possible apps still include the preferred app in the list of apps
+ // the user can choose to handle the type.
+ if (action == Ci.nsIHandlerInfo.useHelperApp) {
+ handlerInfo.preferredApplicationHandler = aActionItem.handlerApp;
+ }
+
+ // Set the "always ask" flag.
+ if (action == Ci.nsIHandlerInfo.alwaysAsk) {
+ handlerInfo.alwaysAskBeforeHandling = true;
+ } else {
+ handlerInfo.alwaysAskBeforeHandling = false;
+ }
+
+ // Set the preferred action.
+ handlerInfo.preferredAction = action;
+
+ handlerInfo.store();
+
+ // Update the action label and image to reflect the new preferred action.
+ this.selectedHandlerListItem.refreshAction();
+ },
+
+ manageApp(aEvent) {
+ // Don't let the normal "on select action" handler get this event,
+ // as we handle it specially ourselves.
+ aEvent.stopPropagation();
+
+ var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
+
+ let onComplete = () => {
+ // Rebuild the actions menu so that we revert to the previous selection,
+ // or "Always ask" if the previous default application has been removed
+ this.rebuildActionsMenu();
+
+ // update the richlistitem too. Will be visible when selecting another row
+ this.selectedHandlerListItem.refreshAction();
+ };
+
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/applicationManager.xhtml",
+ { features: "resizable=no", closingCallback: onComplete },
+ handlerInfo
+ );
+ },
+
+ async chooseApp(aEvent) {
+ // Don't let the normal "on select action" handler get this event,
+ // as we handle it specially ourselves.
+ aEvent.stopPropagation();
+
+ var handlerApp;
+ let chooseAppCallback = aHandlerApp => {
+ // Rebuild the actions menu whether the user picked an app or canceled.
+ // If they picked an app, we want to add the app to the menu and select it.
+ // If they canceled, we want to go back to their previous selection.
+ this.rebuildActionsMenu();
+
+ // If the user picked a new app from the menu, select it.
+ if (aHandlerApp) {
+ let typeItem = this._list.selectedItem;
+ let actionsMenu = typeItem.querySelector(".actionsMenu");
+ let menuItems = actionsMenu.menupopup.childNodes;
+ for (let i = 0; i < menuItems.length; i++) {
+ let menuItem = menuItems[i];
+ if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) {
+ actionsMenu.selectedIndex = i;
+ this.onSelectAction(menuItem);
+ break;
+ }
+ }
+ }
+ };
+
+ if (AppConstants.platform == "win") {
+ var params = {};
+ var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
+
+ params.mimeInfo = handlerInfo.wrappedHandlerInfo;
+ params.title = await document.l10n.formatValue(
+ "applications-select-helper"
+ );
+ if ("id" in handlerInfo.description) {
+ params.description = await document.l10n.formatValue(
+ handlerInfo.description.id,
+ handlerInfo.description.args
+ );
+ } else {
+ params.description = handlerInfo.typeDescription.raw;
+ }
+ params.filename = null;
+ params.handlerApp = null;
+
+ let onAppSelected = () => {
+ if (this.isValidHandlerApp(params.handlerApp)) {
+ handlerApp = params.handlerApp;
+
+ // Add the app to the type's list of possible handlers.
+ handlerInfo.addPossibleApplicationHandler(handlerApp);
+ }
+
+ chooseAppCallback(handlerApp);
+ };
+
+ gSubDialog.open(
+ "chrome://global/content/appPicker.xhtml",
+ { closingCallback: onAppSelected },
+ params
+ );
+ } else {
+ let winTitle = await document.l10n.formatValue(
+ "applications-select-helper"
+ );
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = aResult => {
+ if (
+ aResult == Ci.nsIFilePicker.returnOK &&
+ fp.file &&
+ this._isValidHandlerExecutable(fp.file)
+ ) {
+ handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.name = getFileDisplayName(fp.file);
+ handlerApp.executable = fp.file;
+
+ // Add the app to the type's list of possible handlers.
+ let handler = this.selectedHandlerListItem.handlerInfoWrapper;
+ handler.addPossibleApplicationHandler(handlerApp);
+
+ chooseAppCallback(handlerApp);
+ }
+ };
+
+ // Prompt the user to pick an app. If they pick one, and it's a valid
+ // selection, then add it to the list of possible handlers.
+ fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+ fp.open(fpCallback);
+ }
+ },
+
+ _getIconURLForHandlerApp(aHandlerApp) {
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ return this._getIconURLForFile(aHandlerApp.executable);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
+ return this._getIconURLForWebApp(aHandlerApp.uriTemplate);
+ }
+
+ // We know nothing about other kinds of handler apps.
+ return "";
+ },
+
+ _getIconURLForFile(aFile) {
+ var fph = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ var urlSpec = fph.getURLSpecFromActualFile(aFile);
+
+ return "moz-icon://" + urlSpec + "?size=16";
+ },
+
+ _getIconURLForWebApp(aWebAppURITemplate) {
+ var uri = Services.io.newURI(aWebAppURITemplate);
+
+ // Unfortunately we can't use the favicon service to get the favicon,
+ // because the service looks in the annotations table for a record with
+ // the exact URL we give it, and users won't have such records for URLs
+ // they don't visit, and users won't visit the web app's URL template,
+ // they'll only visit URLs derived from that template (i.e. with %s
+ // in the template replaced by the URL of the content being handled).
+
+ if (
+ /^https?$/.test(uri.scheme) &&
+ Services.prefs.getBoolPref("browser.chrome.site_icons")
+ ) {
+ return uri.prePath + "/favicon.ico";
+ }
+
+ return "";
+ },
+
+ // DOWNLOADS
+
+ /*
+ * Preferences:
+ *
+ * browser.download.useDownloadDir - bool
+ * True - Save files directly to the folder configured via the
+ * browser.download.folderList preference.
+ * False - Always ask the user where to save a file and default to
+ * browser.download.lastDir when displaying a folder picker dialog.
+ * browser.download.always_ask_before_handling_new_types - bool
+ * Defines the default behavior for new file handlers.
+ * True - When downloading a file that doesn't match any existing
+ * handlers, ask the user whether to save or open the file.
+ * False - Save the file. The user can change the default action in
+ * the Applications section in the preferences UI.
+ * browser.download.dir - local file handle
+ * A local folder the user may have selected for downloaded files to be
+ * saved. Migration of other browser settings may also set this path.
+ * This folder is enabled when folderList equals 2.
+ * browser.download.lastDir - local file handle
+ * May contain the last folder path accessed when the user browsed
+ * via the file save-as dialog. (see contentAreaUtils.js)
+ * browser.download.folderList - int
+ * Indicates the location users wish to save downloaded files too.
+ * It is also used to display special file labels when the default
+ * download location is either the Desktop or the Downloads folder.
+ * Values:
+ * 0 - The desktop is the default download location.
+ * 1 - The system's downloads folder is the default download location.
+ * 2 - The default download location is elsewhere as specified in
+ * browser.download.dir.
+ * browser.download.downloadDir
+ * deprecated.
+ * browser.download.defaultFolder
+ * deprecated.
+ */
+
+ /**
+ * Disables the downloads folder field and Browse button if the default
+ * download directory pref is locked (e.g., by the DownloadDirectory or
+ * DefaultDownloadDirectory policies)
+ */
+ readUseDownloadDir() {
+ document.getElementById("downloadFolder").disabled =
+ document.getElementById("chooseFolder").disabled =
+ document.getElementById("saveTo").disabled =
+ Preferences.get("browser.download.dir").locked ||
+ Preferences.get("browser.download.folderList").locked;
+ // don't override the preference's value in UI
+ return undefined;
+ },
+
+ /**
+ * Displays a file picker in which the user can choose the location where
+ * downloads are automatically saved, updating preferences and UI in
+ * response to the choice, if one is made.
+ */
+ chooseFolder() {
+ return this.chooseFolderTask().catch(console.error);
+ },
+ async chooseFolderTask() {
+ let [title] = await document.l10n.formatValues([
+ { id: "choose-download-folder-title" },
+ ]);
+ let folderListPref = Preferences.get("browser.download.folderList");
+ let currentDirPref = await this._indexToFolder(folderListPref.value);
+ let defDownloads = await this._indexToFolder(1);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.init(window, title, Ci.nsIFilePicker.modeGetFolder);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ // First try to open what's currently configured
+ if (currentDirPref && currentDirPref.exists()) {
+ fp.displayDirectory = currentDirPref;
+ } else if (defDownloads && defDownloads.exists()) {
+ // Try the system's download dir
+ fp.displayDirectory = defDownloads;
+ } else {
+ // Fall back to Desktop
+ fp.displayDirectory = await this._indexToFolder(0);
+ }
+
+ let result = await new Promise(resolve => fp.open(resolve));
+ if (result != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ let downloadDirPref = Preferences.get("browser.download.dir");
+ downloadDirPref.value = fp.file;
+ folderListPref.value = await this._folderToIndex(fp.file);
+ // Note, the real prefs will not be updated yet, so dnld manager's
+ // userDownloadsDirectory may not return the right folder after
+ // this code executes. displayDownloadDirPref will be called on
+ // the assignment above to update the UI.
+ },
+
+ /**
+ * Initializes the download folder display settings based on the user's
+ * preferences.
+ */
+ displayDownloadDirPref() {
+ this.displayDownloadDirPrefTask().catch(console.error);
+
+ // don't override the preference's value in UI
+ return undefined;
+ },
+
+ async displayDownloadDirPrefTask() {
+ // We're async for localization reasons, and we can get called several
+ // times in the same turn of the event loop (!) because of how the
+ // preferences bindings work... but the speed of localization
+ // shouldn't impact what gets displayed to the user in the end - the
+ // last call should always win.
+ // To accomplish this, store a unique object when we enter this function,
+ // and if by the end of the function that stored object has been
+ // overwritten, don't update the UI but leave it to the last
+ // caller to this function to do.
+ let token = {};
+ this._downloadDisplayToken = token;
+
+ var downloadFolder = document.getElementById("downloadFolder");
+
+ let folderIndex = Preferences.get("browser.download.folderList").value;
+ // For legacy users using cloudstorage pref with folderIndex as 3 (See bug 1751093),
+ // compute folderIndex using the current directory pref
+ if (folderIndex == 3) {
+ let currentDirPref = Preferences.get("browser.download.dir");
+ folderIndex = currentDirPref.value
+ ? await this._folderToIndex(currentDirPref.value)
+ : 1;
+ }
+
+ // Display a 'pretty' label or the path in the UI.
+ let { folderDisplayName, file } =
+ await this._getSystemDownloadFolderDetails(folderIndex);
+ // Figure out an icon url:
+ let fph = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let iconUrlSpec = fph.getURLSpecFromDir(file);
+
+ // Ensure that the last entry to this function always wins
+ // (see comment at the start of this method):
+ if (this._downloadDisplayToken != token) {
+ return;
+ }
+ // note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user
+ downloadFolder.value = folderDisplayName;
+ downloadFolder.style.backgroundImage =
+ "url(moz-icon://" + iconUrlSpec + "?size=16)";
+ },
+
+ async _getSystemDownloadFolderDetails(folderIndex) {
+ let downloadsDir = await this._getDownloadsFolder("Downloads");
+ let desktopDir = await this._getDownloadsFolder("Desktop");
+ let currentDirPref = Preferences.get("browser.download.dir");
+
+ let file;
+ let firefoxLocalizedName;
+ if (folderIndex == 2 && currentDirPref.value) {
+ file = currentDirPref.value;
+ if (file.equals(downloadsDir)) {
+ folderIndex = 1;
+ } else if (file.equals(desktopDir)) {
+ folderIndex = 0;
+ }
+ }
+ switch (folderIndex) {
+ case 2: // custom path, handled above.
+ break;
+
+ case 1: {
+ // downloads
+ file = downloadsDir;
+ firefoxLocalizedName = await document.l10n.formatValues([
+ { id: "downloads-folder-name" },
+ ]);
+ break;
+ }
+
+ case 0:
+ // fall through
+ default: {
+ file = desktopDir;
+ firefoxLocalizedName = await document.l10n.formatValues([
+ { id: "desktop-folder-name" },
+ ]);
+ }
+ }
+ if (firefoxLocalizedName) {
+ let folderDisplayName, leafName;
+ // Either/both of these can throw, so check for failures in both cases
+ // so we don't just break display of the download pref:
+ try {
+ folderDisplayName = file.displayName;
+ } catch (ex) {
+ /* ignored */
+ }
+ try {
+ leafName = file.leafName;
+ } catch (ex) {
+ /* ignored */
+ }
+
+ // If we found a localized name that's different from the leaf name,
+ // use that:
+ if (folderDisplayName && folderDisplayName != leafName) {
+ return { file, folderDisplayName };
+ }
+
+ // Otherwise, check if we've got a localized name ourselves.
+ if (firefoxLocalizedName) {
+ // You can't move the system download or desktop dir on macOS,
+ // so if those are in use just display them. On other platforms
+ // only do so if the folder matches the localized name.
+ if (
+ AppConstants.platform == "mac" ||
+ leafName == firefoxLocalizedName
+ ) {
+ return { file, folderDisplayName: firefoxLocalizedName };
+ }
+ }
+ }
+ // If we get here, attempts to use a "pretty" name failed. Just display
+ // the full path:
+ if (file) {
+ // Force the left-to-right direction when displaying a custom path.
+ return { file, folderDisplayName: `\u2066${file.path}\u2069` };
+ }
+ // Don't even have a file - fall back to desktop directory for the
+ // use of the icon, and an empty label:
+ file = desktopDir;
+ return { file, folderDisplayName: "" };
+ },
+
+ /**
+ * Returns the Downloads folder. If aFolder is "Desktop", then the Downloads
+ * folder returned is the desktop folder; otherwise, it is a folder whose name
+ * indicates that it is a download folder and whose path is as determined by
+ * the XPCOM directory service via the download manager's attribute
+ * defaultDownloadsDirectory.
+ *
+ * @throws if aFolder is not "Desktop" or "Downloads"
+ */
+ async _getDownloadsFolder(aFolder) {
+ switch (aFolder) {
+ case "Desktop":
+ return Services.dirsvc.get("Desk", Ci.nsIFile);
+ case "Downloads":
+ let downloadsDir = await Downloads.getSystemDownloadsDirectory();
+ return new FileUtils.File(downloadsDir);
+ }
+ throw new Error(
+ "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'"
+ );
+ },
+
+ /**
+ * Determines the type of the given folder.
+ *
+ * @param aFolder
+ * the folder whose type is to be determined
+ * @returns integer
+ * 0 if aFolder is the Desktop or is unspecified,
+ * 1 if aFolder is the Downloads folder,
+ * 2 otherwise
+ */
+ async _folderToIndex(aFolder) {
+ if (!aFolder || aFolder.equals(await this._getDownloadsFolder("Desktop"))) {
+ return 0;
+ } else if (aFolder.equals(await this._getDownloadsFolder("Downloads"))) {
+ return 1;
+ }
+ return 2;
+ },
+
+ /**
+ * Converts an integer into the corresponding folder.
+ *
+ * @param aIndex
+ * an integer
+ * @returns the Desktop folder if aIndex == 0,
+ * the Downloads folder if aIndex == 1,
+ * the folder stored in browser.download.dir
+ */
+ _indexToFolder(aIndex) {
+ switch (aIndex) {
+ case 0:
+ return this._getDownloadsFolder("Desktop");
+ case 1:
+ return this._getDownloadsFolder("Downloads");
+ }
+ var currentDirPref = Preferences.get("browser.download.dir");
+ return currentDirPref.value;
+ },
+};
+
+gMainPane.initialized = new Promise(res => {
+ gMainPane.setInitialized = res;
+});
+
+// Utilities
+
+function getFileDisplayName(file) {
+ if (AppConstants.platform == "win") {
+ if (file instanceof Ci.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+ }
+ if (AppConstants.platform == "macosx") {
+ if (file instanceof Ci.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ }
+ return file.leafName;
+}
+
+function getLocalHandlerApp(aFile) {
+ var localHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.name = getFileDisplayName(aFile);
+ localHandlerApp.executable = aFile;
+
+ return localHandlerApp;
+}
+
+// eslint-disable-next-line no-undef
+let gHandlerListItemFragment = MozXULElement.parseXULToFragment(`
+ <richlistitem>
+ <hbox class="typeContainer" flex="1" align="center">
+ <image class="typeIcon" width="16" height="16"
+ src="moz-icon://goat?size=16"/>
+ <label class="typeDescription" flex="1" crop="end"/>
+ </hbox>
+ <hbox class="actionContainer" flex="1" align="center">
+ <image class="actionIcon" width="16" height="16"/>
+ <label class="actionDescription" flex="1" crop="end"/>
+ </hbox>
+ <hbox class="actionsMenuContainer" flex="1">
+ <menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ </richlistitem>
+`);
+
+/**
+ * This is associated to <richlistitem> elements in the handlers view.
+ */
+class HandlerListItem {
+ static forNode(node) {
+ return gNodeToObjectMap.get(node);
+ }
+
+ constructor(handlerInfoWrapper) {
+ this.handlerInfoWrapper = handlerInfoWrapper;
+ }
+
+ setOrRemoveAttributes(iterable) {
+ for (let [selector, name, value] of iterable) {
+ let node = selector ? this.node.querySelector(selector) : this.node;
+ if (value) {
+ node.setAttribute(name, value);
+ } else {
+ node.removeAttribute(name);
+ }
+ }
+ }
+
+ createNode(list) {
+ list.appendChild(document.importNode(gHandlerListItemFragment, true));
+ this.node = list.lastChild;
+ gNodeToObjectMap.set(this.node, this);
+ }
+
+ setupNode() {
+ this.node
+ .querySelector(".actionsMenu")
+ .addEventListener("command", event =>
+ gMainPane.onSelectAction(event.originalTarget)
+ );
+
+ let typeDescription = this.handlerInfoWrapper.typeDescription;
+ this.setOrRemoveAttributes([
+ [null, "type", this.handlerInfoWrapper.type],
+ [".typeIcon", "src", this.handlerInfoWrapper.smallIcon],
+ ]);
+ localizeElement(
+ this.node.querySelector(".typeDescription"),
+ typeDescription
+ );
+ this.showActionsMenu = false;
+ }
+
+ refreshAction() {
+ let { actionIconClass } = this.handlerInfoWrapper;
+ this.setOrRemoveAttributes([
+ [null, APP_ICON_ATTR_NAME, actionIconClass],
+ [
+ ".actionIcon",
+ "src",
+ actionIconClass ? null : this.handlerInfoWrapper.actionIcon,
+ ],
+ ]);
+ const selectedItem = this.node.querySelector("[selected=true]");
+ if (!selectedItem) {
+ console.error("No selected item for " + this.handlerInfoWrapper.type);
+ return;
+ }
+ const { id, args } = document.l10n.getAttributes(selectedItem);
+ localizeElement(this.node.querySelector(".actionDescription"), {
+ id: id + "-label",
+ args,
+ });
+ localizeElement(this.node.querySelector(".actionsMenu"), { id, args });
+ }
+
+ set showActionsMenu(value) {
+ this.setOrRemoveAttributes([
+ [".actionContainer", "hidden", value],
+ [".actionsMenuContainer", "hidden", !value],
+ ]);
+ }
+}
+
+/**
+ * This API facilitates dual-model of some localization APIs which
+ * may operate on raw strings of l10n id/args pairs.
+ *
+ * The l10n can be:
+ *
+ * {raw: string} - raw strings to be used as text value of the element
+ * {id: string} - l10n-id
+ * {id: string, args: object} - l10n-id + l10n-args
+ */
+function localizeElement(node, l10n) {
+ if (l10n.hasOwnProperty("raw")) {
+ node.removeAttribute("data-l10n-id");
+ node.textContent = l10n.raw;
+ } else {
+ document.l10n.setAttributes(node, l10n.id, l10n.args);
+ }
+}
+
+/**
+ * This object wraps nsIHandlerInfo with some additional functionality
+ * the Applications prefpane needs to display and allow modification of
+ * the list of handled types.
+ *
+ * We create an instance of this wrapper for each entry we might display
+ * in the prefpane, and we compose the instances from various sources,
+ * including the handler service.
+ *
+ * We don't implement all the original nsIHandlerInfo functionality,
+ * just the stuff that the prefpane needs.
+ */
+class HandlerInfoWrapper {
+ constructor(type, handlerInfo) {
+ this.type = type;
+ this.wrappedHandlerInfo = handlerInfo;
+ this.disambiguateDescription = false;
+ }
+
+ get description() {
+ if (this.wrappedHandlerInfo.description) {
+ return { raw: this.wrappedHandlerInfo.description };
+ }
+
+ if (this.primaryExtension) {
+ var extension = this.primaryExtension.toUpperCase();
+ return { id: "applications-file-ending", args: { extension } };
+ }
+
+ return { raw: this.type };
+ }
+
+ /**
+ * Describe, in a human-readable fashion, the type represented by the given
+ * handler info object. Normally this is just the description, but if more
+ * than one object presents the same description, "disambiguateDescription"
+ * is set and we annotate the duplicate descriptions with the type itself
+ * to help users distinguish between those types.
+ */
+ get typeDescription() {
+ if (this.disambiguateDescription) {
+ const description = this.description;
+ if (description.id) {
+ // Pass through the arguments:
+ let { args = {} } = description;
+ args.type = this.type;
+ return {
+ id: description.id + "-with-type",
+ args,
+ };
+ }
+
+ return {
+ id: "applications-type-description-with-type",
+ args: {
+ "type-description": description.raw,
+ type: this.type,
+ },
+ };
+ }
+
+ return this.description;
+ }
+
+ get actionIconClass() {
+ if (this.alwaysAskBeforeHandling) {
+ return "ask";
+ }
+
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.saveToDisk:
+ return "save";
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (this instanceof InternalHandlerInfoWrapper) {
+ return "handleInternally";
+ }
+ break;
+ }
+
+ return "";
+ }
+
+ get actionIcon() {
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ return this.iconURLForSystemDefault;
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ let preferredApp = this.preferredApplicationHandler;
+ if (gMainPane.isValidHandlerApp(preferredApp)) {
+ return gMainPane._getIconURLForHandlerApp(preferredApp);
+ }
+
+ // This should never happen, but if preferredAction is set to some weird
+ // value, then fall back to the generic application icon.
+ // Explicit fall-through
+ default:
+ return ICON_URL_APP;
+ }
+ }
+
+ get iconURLForSystemDefault() {
+ // Handler info objects for MIME types on some OSes implement a property bag
+ // interface from which we can get an icon for the default app, so if we're
+ // dealing with a MIME type on one of those OSes, then try to get the icon.
+ if (
+ this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo instanceof Ci.nsIPropertyBag
+ ) {
+ try {
+ let url = this.wrappedHandlerInfo.getProperty(
+ "defaultApplicationIconURL"
+ );
+ if (url) {
+ return url + "?size=16";
+ }
+ } catch (ex) {}
+ }
+
+ // If this isn't a MIME type object on an OS that supports retrieving
+ // the icon, or if we couldn't retrieve the icon for some other reason,
+ // then use a generic icon.
+ return ICON_URL_APP;
+ }
+
+ get preferredApplicationHandler() {
+ return this.wrappedHandlerInfo.preferredApplicationHandler;
+ }
+
+ set preferredApplicationHandler(aNewValue) {
+ this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue;
+
+ // Make sure the preferred handler is in the set of possible handlers.
+ if (aNewValue) {
+ this.addPossibleApplicationHandler(aNewValue);
+ }
+ }
+
+ get possibleApplicationHandlers() {
+ return this.wrappedHandlerInfo.possibleApplicationHandlers;
+ }
+
+ addPossibleApplicationHandler(aNewHandler) {
+ for (let app of this.possibleApplicationHandlers.enumerate()) {
+ if (app.equals(aNewHandler)) {
+ return;
+ }
+ }
+ this.possibleApplicationHandlers.appendElement(aNewHandler);
+ }
+
+ removePossibleApplicationHandler(aHandler) {
+ var defaultApp = this.preferredApplicationHandler;
+ if (defaultApp && aHandler.equals(defaultApp)) {
+ // If the app we remove was the default app, we must make sure
+ // it won't be used anymore
+ this.alwaysAskBeforeHandling = true;
+ this.preferredApplicationHandler = null;
+ }
+
+ var handlers = this.possibleApplicationHandlers;
+ for (var i = 0; i < handlers.length; ++i) {
+ var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ if (handler.equals(aHandler)) {
+ handlers.removeElementAt(i);
+ break;
+ }
+ }
+ }
+
+ get hasDefaultHandler() {
+ return this.wrappedHandlerInfo.hasDefaultHandler;
+ }
+
+ get defaultDescription() {
+ return this.wrappedHandlerInfo.defaultDescription;
+ }
+
+ // What to do with content of this type.
+ get preferredAction() {
+ // If the action is to use a helper app, but we don't have a preferred
+ // handler app, then switch to using the system default, if any; otherwise
+ // fall back to saving to disk, which is the default action in nsMIMEInfo.
+ // Note: "save to disk" is an invalid value for protocol info objects,
+ // but the alwaysAskBeforeHandling getter will detect that situation
+ // and always return true in that case to override this invalid value.
+ if (
+ this.wrappedHandlerInfo.preferredAction ==
+ Ci.nsIHandlerInfo.useHelperApp &&
+ !gMainPane.isValidHandlerApp(this.preferredApplicationHandler)
+ ) {
+ if (this.wrappedHandlerInfo.hasDefaultHandler) {
+ return Ci.nsIHandlerInfo.useSystemDefault;
+ }
+ return Ci.nsIHandlerInfo.saveToDisk;
+ }
+
+ return this.wrappedHandlerInfo.preferredAction;
+ }
+
+ set preferredAction(aNewValue) {
+ this.wrappedHandlerInfo.preferredAction = aNewValue;
+ }
+
+ get alwaysAskBeforeHandling() {
+ // If this is a protocol type and the preferred action is "save to disk",
+ // which is invalid for such types, then return true here to override that
+ // action. This could happen when the preferred action is to use a helper
+ // app, but the preferredApplicationHandler is invalid, and there isn't
+ // a default handler, so the preferredAction getter returns save to disk
+ // instead.
+ if (
+ !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) &&
+ this.preferredAction == Ci.nsIHandlerInfo.saveToDisk
+ ) {
+ return true;
+ }
+
+ return this.wrappedHandlerInfo.alwaysAskBeforeHandling;
+ }
+
+ set alwaysAskBeforeHandling(aNewValue) {
+ this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue;
+ }
+
+ // The primary file extension associated with this type, if any.
+ get primaryExtension() {
+ try {
+ if (
+ this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo.primaryExtension
+ ) {
+ return this.wrappedHandlerInfo.primaryExtension;
+ }
+ } catch (ex) {}
+
+ return null;
+ }
+
+ store() {
+ gHandlerService.store(this.wrappedHandlerInfo);
+ }
+
+ get smallIcon() {
+ return this._getIcon(16);
+ }
+
+ _getIcon(aSize) {
+ if (this.primaryExtension) {
+ return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize;
+ }
+
+ if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type;
+ }
+
+ // FIXME: consider returning some generic icon when we can't get a URL for
+ // one (for example in the case of protocol schemes). Filed as bug 395141.
+ return null;
+ }
+}
+
+/**
+ * InternalHandlerInfoWrapper provides a basic mechanism to create an internal
+ * mime type handler that can be enabled/disabled in the applications preference
+ * menu.
+ */
+class InternalHandlerInfoWrapper extends HandlerInfoWrapper {
+ constructor(mimeType, extension) {
+ let type = gMIMEService.getFromTypeAndExtension(mimeType, extension);
+ super(mimeType || type.type, type);
+ }
+
+ // Override store so we so we can notify any code listening for registration
+ // or unregistration of this handler.
+ store() {
+ super.store();
+ }
+
+ get preventInternalViewing() {
+ return false;
+ }
+
+ get enabled() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+}
+
+class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper {
+ constructor() {
+ super(TYPE_PDF, null);
+ }
+
+ get preventInternalViewing() {
+ return Services.prefs.getBoolPref(PREF_PDFJS_DISABLED);
+ }
+
+ // PDF is always shown in the list, but the 'show internally' option is
+ // hidden when the internal PDF viewer is disabled.
+ get enabled() {
+ return true;
+ }
+}
+
+class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper {
+ get enabled() {
+ return DownloadIntegration.shouldViewDownloadInternally(this.type);
+ }
+}
+
+const AppearanceChooser = {
+ // NOTE: This order must match the values of the
+ // layout.css.prefers-color-scheme.content-override
+ // preference.
+ choices: ["dark", "light", "auto"],
+ chooser: null,
+ radios: null,
+ warning: null,
+
+ init() {
+ this.chooser = document.getElementById("web-appearance-chooser");
+ this.radios = [...this.chooser.querySelectorAll("input")];
+ for (let radio of this.radios) {
+ radio.addEventListener("change", e => {
+ let index = this.choices.indexOf(e.target.value);
+ // The pref change callback will update state if needed.
+ if (index >= 0) {
+ Services.prefs.setIntPref(PREF_CONTENT_APPEARANCE, index);
+ } else {
+ // Shouldn't happen but let's do something sane...
+ Services.prefs.clearUserPref(PREF_CONTENT_APPEARANCE);
+ }
+ });
+ }
+
+ // Forward the click to the "colors" button.
+ document
+ .getElementById("web-appearance-manage-colors-link")
+ .addEventListener("click", function (e) {
+ document.getElementById("colors").click();
+ e.preventDefault();
+ });
+
+ document
+ .getElementById("web-appearance-manage-themes-link")
+ .addEventListener("click", function (e) {
+ window.browsingContext.topChromeWindow.BrowserOpenAddonsMgr(
+ "addons://list/theme"
+ );
+ e.preventDefault();
+ });
+
+ this.warning = document.getElementById("web-appearance-override-warning");
+
+ FORCED_COLORS_QUERY.addEventListener("change", this);
+ Services.prefs.addObserver(PREF_USE_SYSTEM_COLORS, this);
+ Services.obs.addObserver(this, "look-and-feel-changed");
+ this._update();
+ },
+
+ _update() {
+ this._updateWarning();
+ this._updateOptions();
+ },
+
+ handleEvent(e) {
+ this._update();
+ },
+
+ observe(subject, topic, data) {
+ this._update();
+ },
+
+ destroy() {
+ Services.prefs.removeObserver(PREF_USE_SYSTEM_COLORS, this);
+ Services.obs.removeObserver(this, "look-and-feel-changed");
+ FORCED_COLORS_QUERY.removeEventListener("change", this);
+ },
+
+ _isValueDark(value) {
+ switch (value) {
+ case "light":
+ return false;
+ case "dark":
+ return true;
+ case "auto":
+ return Services.appinfo.contentThemeDerivedColorSchemeIsDark;
+ }
+ throw new Error("Unknown value");
+ },
+
+ _updateOptions() {
+ let index = Services.prefs.getIntPref(PREF_CONTENT_APPEARANCE);
+ if (index < 0 || index >= this.choices.length) {
+ index = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref(PREF_CONTENT_APPEARANCE);
+ }
+ let value = this.choices[index];
+ for (let radio of this.radios) {
+ let checked = radio.value == value;
+ let isDark = this._isValueDark(radio.value);
+
+ radio.checked = checked;
+ radio.closest("label").classList.toggle("dark", isDark);
+ }
+ },
+
+ _updateWarning() {
+ let forcingColorsAndNoColorSchemeSupport =
+ FORCED_COLORS_QUERY.matches &&
+ (AppConstants.platform == "win" ||
+ !Services.prefs.getBoolPref(PREF_USE_SYSTEM_COLORS));
+ this.warning.hidden = !forcingColorsAndNoColorSchemeSupport;
+ },
+};