gecko-dev/browser/components/preferences/main.js

3622 строки
119 KiB
JavaScript

/* 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 ../../base/content/aboutDialog-appUpdater.js */
/* global MozXULElement */
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { Downloads } = ChromeUtils.import("resource://gre/modules/Downloads.jsm");
var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
var { ShortcutUtils } = ChromeUtils.import(
"resource://gre/modules/ShortcutUtils.jsm"
);
var { TransientPrefs } = ChromeUtils.import(
"resource:///modules/TransientPrefs.jsm"
);
var { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
var { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm");
ChromeUtils.defineModuleGetter(
this,
"CloudStorage",
"resource://gre/modules/CloudStorage.jsm"
);
var { Integration } = ChromeUtils.import(
"resource://gre/modules/Integration.jsm"
);
/* global DownloadIntegration */
Integration.downloads.defineModuleGetter(
this,
"DownloadIntegration",
"resource://gre/modules/DownloadIntegration.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"SelectionChangedMenulist",
"resource:///modules/SelectionChangedMenulist.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"UpdateUtils",
"resource://gre/modules/UpdateUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetters(this, {
gApplicationUpdateService: [
"@mozilla.org/updates/update-service;1",
"nsIApplicationUpdateService",
],
gHandlerService: [
"@mozilla.org/uriloader/handler-service;1",
"nsIHandlerService",
],
gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
});
// 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 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";
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
if (AppConstants.MOZ_DEV_EDITION) {
ChromeUtils.defineModuleGetter(
this,
"fxAccounts",
"resource://gre/modules/FxAccounts.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccounts",
"resource://gre/modules/FxAccounts.jsm"
);
}
Preferences.addAll([
// Startup
{ id: "browser.startup.page", type: "int" },
{ id: "browser.privatebrowsing.autostart", type: "bool" },
// Downloads
{ id: "browser.download.useDownloadDir", 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: "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.MOZ_MAINTENANCE_SERVICE) {
Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]);
}
}
XPCOMUtils.defineLazyGetter(this, "gHasWinPackageId", () => {
let hasWinPackageId = false;
try {
hasWinPackageId = Services.sysinfo.getProperty("hasWinPackageId");
} catch (_ex) {
// The hasWinPackageId property doesn't exist; assume it would be false.
}
return hasWinPackageId;
});
// 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();
let performanceSettingsLink = document.getElementById(
"performanceSettingsLearnMore"
);
let performanceSettingsUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"performance";
performanceSettingsLink.setAttribute("href", performanceSettingsUrl);
this.updateDefaultPerformanceSettingsPref();
let defaultPerformancePref = Preferences.get(
"browser.preferences.defaultPerformanceSettings.enabled"
);
defaultPerformancePref.on("change", () => {
this.updatePerformanceSettingsBox({ duringChangeEvent: true });
});
this.updatePerformanceSettingsBox({ duringChangeEvent: false });
this.displayUseSystemLocale();
let connectionSettingsLink = document.getElementById(
"connectionSettingsLearnMore"
);
let connectionSettingsUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"prefs-connection-settings";
connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
this.updateProxySettingsUI();
initializeProxyUI(gMainPane);
if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
gMainPane.initBrowserLocale();
}
// We call `initDefaultZoomValues` to set and unhide the
// default zoom preferences menu, and to establish a
// listener for future menu changes.
gMainPane.initDefaultZoomValues();
let cfrLearnMoreUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"extensionrecommendations";
for (const id of ["cfrLearnMore", "cfrFeaturesLearnMore"]) {
let link = document.getElementById(id);
link.setAttribute("href", cfrLearnMoreUrl);
}
if (
Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.enabled"
)
) {
document.getElementById("pictureInPictureBox").hidden = false;
let pipLearnMoreUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"picture-in-picture";
let link = document.getElementById("pictureInPictureLearnMore");
link.setAttribute("href", pipLearnMoreUrl);
}
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.showBrowserLanguages({ 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();
});
}
// 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
);
setEventListener(
"translateButton",
"command",
gMainPane.showTranslationExceptions
);
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
);
// 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;
let mediaControlLearnMoreUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"media-keyboard-control";
let link = document.getElementById("mediaControlLearnMore");
link.setAttribute("href", mediaControlLearnMoreUrl);
}
// 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
const fxtranslationsDisabledPrefName = "extensions.translations.disabled";
if (!Services.prefs.getBoolPref(fxtranslationsDisabledPrefName, true)) {
let fxtranslationRow = document.getElementById("fxtranslationsBox");
fxtranslationRow.hidden = false;
}
let drmInfoURL =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"drm-content";
document
.getElementById("playDRMContentLink")
.setAttribute("href", drmInfoURL);
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 distroId = Services.prefs.getCharPref("distribution.id", "");
if (distroId) {
let distroString = distroId;
let distroVersion = Services.prefs.getCharPref(
"distribution.version",
""
);
if (distroVersion) {
distroString += " - " + distroVersion;
}
let distroIdField = document.getElementById("distributionId");
distroIdField.value = distroString;
distroIdField.hidden = false;
let distroAbout = Services.prefs.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 (gHasWinPackageId) {
// 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);
setEventListener("saveWhere", "command", gMainPane.handleSaveToCommand);
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"),
]);
// 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("saveWhere"),
() => 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 }
);
});
},
// 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);
const link = document.getElementById("browserContainersLearnMore");
link.href =
Services.urlFormatter.formatURLPref("app.support.baseURL") + "containers";
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;
}
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;
},
initBrowserLocale() {
// Enable telemetry.
Services.telemetry.setEventRecordingEnabled(
"intl.ui.browserLanguage",
true
);
// This will register the "command" listener.
let menulist = document.getElementById("defaultBrowserLanguage");
new SelectionChangedMenulist(menulist, event => {
gMainPane.onBrowserLanguageChange(event);
});
gMainPane.setBrowserLocales(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.
*/
async setBrowserLocales(selected) {
let available = await getAvailableLocales();
let localeNames = Services.intl.getLocaleDisplayNames(undefined, available);
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 = "defaultBrowserLanguageSearch";
menuitem.setAttribute(
"label",
await document.l10n.formatValue("browser-languages-search")
);
menuitem.setAttribute("value", "search");
fragment.appendChild(menuitem);
}
let menulist = document.getElementById("defaultBrowserLanguage");
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.setAttribute("flex", "1");
messageContainer.setAttribute("align", "center");
let description = document.createXULElement("description");
description.classList.add("message-bar-description");
// TODO: This should preferably use `Intl.LocaleInfo` when bug 1693576 is fixed.
if (
i == 0 &&
(locales[0] == "ar" ||
locales[0] == "ckb" ||
locales[0] == "fa" ||
locales[0] == "he" ||
locales[0] == "ur")
) {
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.selectedLocales = 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. */
onBrowserLanguageChange(event) {
let locale = event.target.value;
if (locale == "search") {
gMainPane.showBrowserLanguages({ search: true });
return;
} else if (locale == Services.locale.appLocaleAsBCP47) {
this.hideConfirmLanguageChangeMessageBar();
return;
}
// Note the change in telemetry.
gMainPane.recordBrowserLanguagesTelemetry("reorder");
let locales = Array.from(
new Set([locale, ...Services.locale.requestedLocales]).values()
);
this.showConfirmLanguageChangeMessageBar(locales);
},
/**
* 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");
if (!shellSvc) {
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) {
Cu.reportError(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
);
},
showBrowserLanguages({ 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 = { selected: gMainPane.selectedLocales, search, telemetryId };
gSubDialog.open(
"chrome://browser/content/preferences/dialogs/browserLanguages.xhtml",
{ closingCallback: this.browserLanguagesClosed },
opts
);
},
/* Show or hide the confirm change message bar based on the updated ordering. */
browserLanguagesClosed() {
let { accepted, selected } = this.gBrowserLanguagesDialog;
let active = Services.locale.appLocalesAsBCP47;
this.gBrowserLanguagesDialog.recordTelemetry(
accepted ? "accept" : "cancel"
);
// Prepare for changing the locales if they are different than the current locales.
if (selected && selected.join(",") != active.join(",")) {
gMainPane.showConfirmLanguageChangeMessageBar(selected);
gMainPane.setBrowserLocales(selected[0]);
return;
}
// They matched, so we can reset the UI.
gMainPane.setBrowserLocales(Services.locale.appLocaleAsBCP47);
gMainPane.hideConfirmLanguageChangeMessageBar();
},
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,
]);
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.
*/
showTranslationExceptions() {
gSubDialog.open(
"chrome://browser/content/preferences/dialogs/translation.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(Cu.reportError);
},
/**
* 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;
}
},
/**
* Selects the correct item in the update radio group
*/
async readUpdateAutoPref() {
if (
AppConstants.MOZ_UPDATER &&
(!Services.policies || Services.policies.isAllowed("appUpdate")) &&
!gHasWinPackageId
) {
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")) &&
!gHasWinPackageId
) {
let radiogroup = document.getElementById("updateRadioGroup");
let updateAutoValue = radiogroup.value == "true";
radiogroup.disabled = true;
try {
await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue);
radiogroup.disabled = false;
} catch (error) {
Cu.reportError(error);
await this.readUpdateAutoPref();
await this.reportUpdatePrefWriteError(error);
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();
}
}
},
isBackgroundUpdateUIAvailable() {
return (
Services.prefs.getBoolPref(
"app.update.background.scheduling.enabled",
false
) &&
AppConstants.MOZ_UPDATER &&
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")) &&
!gHasWinPackageId &&
!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;
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) {
Cu.reportError(error);
await this.readBackgroundUpdatePref();
await this.reportUpdatePrefWriteError(error);
return;
}
this.maybeDisableBackgroundUpdateControls();
}
},
async reportUpdatePrefWriteError(error) {
let [title, message] = await document.l10n.formatValues([
{ id: "update-setting-write-failure-title2" },
{
id: "update-setting-write-failure-message2",
args: { path: error.path },
},
]);
// 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);
},
// 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_UPDATER || !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(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 {
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) {
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 (Cc["@mozilla.org/gio-service;1"]) {
let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService(
Ci.nsIGIOService
);
var gioApps = gIOSvc.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 {
Cu.reportError("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(", ");
Cu.reportError(
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) {
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.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.
* 3 - The default download location is elsewhere as specified by
* cloud storage API getDownloadFolder
* browser.download.downloadDir
* deprecated.
* browser.download.defaultFolder
* deprecated.
*/
/**
* Enables/disables the folder field and Browse button based on whether a
* default download directory is being used.
*/
readUseDownloadDir() {
var downloadFolder = document.getElementById("downloadFolder");
var chooseFolder = document.getElementById("chooseFolder");
var useDownloadDirPreference = Preferences.get(
"browser.download.useDownloadDir"
);
var dirPreference = Preferences.get("browser.download.dir");
downloadFolder.disabled =
!useDownloadDirPreference.value || dirPreference.locked;
chooseFolder.disabled =
!useDownloadDirPreference.value || dirPreference.locked;
this.readCloudStorage().catch(Cu.reportError);
// don't override the preference's value in UI
return undefined;
},
/**
* Show/Hide the cloud storage radio button with provider name as label if
* cloud storage provider is in use.
* Select cloud storage radio button if browser.download.useDownloadDir is true
* and browser.download.folderList has value 3. Enables/disables the folder field
* and Browse button if cloud storage radio button is selected.
*
*/
async readCloudStorage() {
// Get preferred provider in use display name
let providerDisplayName = await CloudStorage.getProviderIfInUse();
if (providerDisplayName) {
// Show cloud storage radio button with provider name in label
let saveToCloudRadio = document.getElementById("saveToCloud");
document.l10n.setAttributes(
saveToCloudRadio,
"save-files-to-cloud-storage",
{
"service-name": providerDisplayName,
}
);
saveToCloudRadio.hidden = false;
let useDownloadDirPref = Preferences.get(
"browser.download.useDownloadDir"
);
let folderListPref = Preferences.get("browser.download.folderList");
// Check if useDownloadDir is true and folderListPref is set to Cloud Storage value 3
// before selecting cloudStorageradio button. Disable folder field and Browse button if
// 'Save to Cloud Storage Provider' radio option is selected
if (useDownloadDirPref.value && folderListPref.value === 3) {
document.getElementById("saveWhere").selectedItem = saveToCloudRadio;
document.getElementById("downloadFolder").disabled = true;
document.getElementById("chooseFolder").disabled = true;
}
}
},
/**
* Handle clicks to 'Save To <custom path> or <system default downloads>' and
* 'Save to <cloud storage provider>' if cloud storage radio button is displayed in UI.
* Sets browser.download.folderList value and Enables/disables the folder field and Browse
* button based on option selected.
*/
handleSaveToCommand(event) {
return this.handleSaveToCommandTask(event).catch(Cu.reportError);
},
async handleSaveToCommandTask(event) {
if (event.target.id !== "saveToCloud" && event.target.id !== "saveTo") {
return;
}
// Check if Save To Cloud Storage Provider radio option is displayed in UI
// before continuing.
let saveToCloudRadio = document.getElementById("saveToCloud");
if (!saveToCloudRadio.hidden) {
// When switching between SaveTo and SaveToCloud radio button
// with useDownloadDirPref value true, if selectedIndex is other than
// SaveTo radio button disable downloadFolder filefield and chooseFolder button
let saveWhere = document.getElementById("saveWhere");
let useDownloadDirPref = Preferences.get(
"browser.download.useDownloadDir"
);
if (useDownloadDirPref.value) {
let downloadFolder = document.getElementById("downloadFolder");
let chooseFolder = document.getElementById("chooseFolder");
downloadFolder.disabled =
saveWhere.selectedIndex || useDownloadDirPref.locked;
chooseFolder.disabled =
saveWhere.selectedIndex || useDownloadDirPref.locked;
}
// Set folderListPref value depending on radio option
// selected. folderListPref should be set to 3 if Save To Cloud Storage Provider
// option is selected. If user switch back to 'Save To' custom path or system
// default Downloads, check pref 'browser.download.dir' before setting respective
// folderListPref value. If currentDirPref is unspecified folderList should
// default to 1
let folderListPref = Preferences.get("browser.download.folderList");
let saveTo = document.getElementById("saveTo");
if (saveWhere.selectedItem == saveToCloudRadio) {
folderListPref.value = 3;
} else if (saveWhere.selectedItem == saveTo) {
let currentDirPref = Preferences.get("browser.download.dir");
folderListPref.value = currentDirPref.value
? await this._folderToIndex(currentDirPref.value)
: 1;
}
}
},
/**
* 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(Cu.reportError);
},
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(Cu.reportError);
// don't override the preference's value in UI
return undefined;
},
async displayDownloadDirPrefTask() {
var folderListPref = Preferences.get("browser.download.folderList");
var downloadFolder = document.getElementById("downloadFolder");
var currentDirPref = Preferences.get("browser.download.dir");
// Used in defining the correct path to the folder icon.
var fph = Services.io
.getProtocolHandler("file")
.QueryInterface(Ci.nsIFileProtocolHandler);
var iconUrlSpec;
let folderIndex = folderListPref.value;
if (folderIndex == 3) {
// When user has selected cloud storage, use value in currentDirPref to
// compute index to display download folder label and icon to avoid
// displaying blank downloadFolder label and icon on load of preferences UI
// Set folderIndex to 1 if currentDirPref is unspecified
folderIndex = currentDirPref.value
? await this._folderToIndex(currentDirPref.value)
: 1;
}
// Display a 'pretty' label or the path in the UI.
// note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user
if (folderIndex == 2) {
// Force the left-to-right direction when displaying a custom path.
downloadFolder.value = currentDirPref.value
? `\u2066${currentDirPref.value.path}\u2069`
: "";
iconUrlSpec = fph.getURLSpecFromDir(currentDirPref.value);
} else if (folderIndex == 1) {
// 'Downloads'
[downloadFolder.value] = await document.l10n.formatValues([
{ id: "downloads-folder-name" },
]);
iconUrlSpec = fph.getURLSpecFromDir(await this._indexToFolder(1));
} else {
// 'Desktop'
[downloadFolder.value] = await document.l10n.formatValues([
{ id: "desktop-folder-name" },
]);
iconUrlSpec = fph.getURLSpecFromDir(
await this._getDownloadsFolder("Desktop")
);
}
downloadFolder.style.backgroundImage =
"url(moz-icon://" + iconUrlSpec + "?size=16)";
},
/**
* 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 flex="1" equalsize="always">
<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>
</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) {
Cu.reportError("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 enabled() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
}
class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper {
constructor() {
super(TYPE_PDF, null);
}
get enabled() {
return !Services.prefs.getBoolPref(PREF_PDFJS_DISABLED);
}
}
class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper {
constructor(extension) {
super(null, extension);
}
get enabled() {
return DownloadIntegration.shouldViewDownloadInternally(this.type);
}
}