2645 lines
84 KiB
JavaScript
2645 lines
84 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 MozBoxItem from 'moz-src:///toolkit/content/widgets/moz-box-item/moz-box-item.mjs';*/
|
|
/** @import { MozOption } from 'moz-src:///toolkit/content/widgets/moz-select/moz-select.mjs';*/
|
|
/** @import MozSelect from 'moz-src:///toolkit/content/widgets/moz-select/moz-select.mjs';*/
|
|
/** @import MozBoxGroup from 'chrome://global/content/elements/moz-box-group.mjs'; */
|
|
/** @import { AsyncSettingHandler } from 'chrome://global/content/preferences/AsyncSetting.mjs'; */
|
|
/** @import { HandlerInfoWrapper, ApplicationListItem } from './config/downloads.mjs';*/
|
|
|
|
/* import-globals-from extensionControlled.js */
|
|
/* import-globals-from preferences.js */
|
|
/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
|
|
/* import-globals-from /browser/base/content/aboutDialog-appUpdater.js */
|
|
/* global MozXULElement */
|
|
|
|
/**
|
|
* @import { Setting } from "chrome://global/content/preferences/Setting.mjs"
|
|
*/
|
|
|
|
const { Multilingual } = ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/config/languages.mjs",
|
|
{ global: "current" }
|
|
);
|
|
|
|
const { DefaultBrowserHelper } = ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/DefaultBrowserHelper.mjs",
|
|
{ global: "current" }
|
|
);
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.sys.mjs",
|
|
UpdateListener: "resource://gre/modules/UpdateListener.sys.mjs",
|
|
LinkPreview: "moz-src:///browser/components/genai/LinkPreview.sys.mjs",
|
|
MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
|
|
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
|
|
TranslationsUtils:
|
|
"chrome://global/content/translations/TranslationsUtils.mjs",
|
|
WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
|
|
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
|
FormAutofillPreferences:
|
|
"resource://autofill/FormAutofillPreferences.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/config/accessibility.mjs",
|
|
{ global: "current" }
|
|
);
|
|
ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/config/about-firefox.mjs",
|
|
{ global: "current" }
|
|
);
|
|
|
|
ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/config/appearance.mjs",
|
|
{ global: "current" }
|
|
);
|
|
|
|
ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/config/tabs-browsing.mjs",
|
|
{ global: "current" }
|
|
);
|
|
|
|
// 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 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";
|
|
|
|
const OPEN_EXTERNAL_LINK_NEXT_TO_ACTIVE_TAB_VALUE =
|
|
Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT;
|
|
|
|
/**
|
|
* @param {Setting} featureSetting
|
|
* @param {Setting} defaultSetting
|
|
*/
|
|
function canShowAiFeature(featureSetting, defaultSetting) {
|
|
return (
|
|
featureSetting.value != "blocked" &&
|
|
!(featureSetting.value == "default" && defaultSetting.value == "blocked")
|
|
);
|
|
}
|
|
|
|
Preferences.addAll([
|
|
// Startup
|
|
{ id: "browser.startup.page", type: "int" },
|
|
{ id: "browser.sessionstore.newTabOnRestore", type: "bool" },
|
|
{ id: "browser.startup.windowsLaunchOnLogin.enabled", type: "bool" },
|
|
{ id: "browser.privatebrowsing.autostart", type: "bool" },
|
|
|
|
// AI Controls, these pref values can affect settings on the main pane and
|
|
// have base Settings here
|
|
{ id: "browser.ai.control.default", type: "string" },
|
|
{ id: "browser.ai.control.translations", type: "string" },
|
|
{ id: "browser.ai.control.pdfjsAltText", type: "string" },
|
|
{ id: "browser.ai.control.smartTabGroups", type: "string" },
|
|
{ id: "browser.ai.control.linkPreviewKeyPoints", type: "string" },
|
|
{ id: "browser.ai.control.sidebarChatbot", type: "string" },
|
|
{ id: "browser.ai.control.smartWindow", type: "string" },
|
|
|
|
// Update
|
|
{ id: "browser.preferences.advanced.selectedTabIndex", type: "int" },
|
|
{ id: "browser.search.update", type: "bool" },
|
|
|
|
{
|
|
id: "privacy.userContext.newTabContainerOnLeftClick.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" },
|
|
]);
|
|
}
|
|
|
|
Preferences.addSetting({
|
|
id: "privateBrowsingAutoStart",
|
|
pref: "browser.privatebrowsing.autostart",
|
|
});
|
|
|
|
Preferences.addSetting(
|
|
/** @type {{ _getLaunchOnLoginApprovedCachedValue: boolean } & SettingConfig} */ ({
|
|
id: "launchOnLoginApproved",
|
|
_getLaunchOnLoginApprovedCachedValue: true,
|
|
get() {
|
|
return this._getLaunchOnLoginApprovedCachedValue;
|
|
},
|
|
// Check for a launch on login registry key
|
|
// This accounts for if a user manually changes it in the registry
|
|
// Disabling in Task Manager works outside of just deleting the registry key
|
|
// in HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run
|
|
// but it is not possible to change it back to enabled as the disabled value is just a random
|
|
// hexadecimal number
|
|
setup() {
|
|
if (AppConstants.platform !== "win") {
|
|
/**
|
|
* WindowsLaunchOnLogin isnt available if not on windows
|
|
* but this setup function still fires, so must prevent
|
|
* WindowsLaunchOnLogin.getLaunchOnLoginApproved
|
|
* below from executing unnecessarily.
|
|
*/
|
|
return;
|
|
}
|
|
// @ts-ignore bug 1996860
|
|
WindowsLaunchOnLogin.getLaunchOnLoginApproved().then(val => {
|
|
this._getLaunchOnLoginApprovedCachedValue = val;
|
|
});
|
|
},
|
|
})
|
|
);
|
|
|
|
Preferences.addSetting({
|
|
id: "windowsLaunchOnLoginEnabled",
|
|
pref: "browser.startup.windowsLaunchOnLogin.enabled",
|
|
});
|
|
|
|
Preferences.addSetting(
|
|
/** @type {{_getLaunchOnLoginEnabledValue: boolean, startWithLastProfile: boolean} & SettingConfig} */ ({
|
|
id: "windowsLaunchOnLogin",
|
|
deps: ["launchOnLoginApproved", "windowsLaunchOnLoginEnabled"],
|
|
_getLaunchOnLoginEnabledValue: false,
|
|
get startWithLastProfile() {
|
|
return Cc["@mozilla.org/toolkit/profile-service;1"].getService(
|
|
Ci.nsIToolkitProfileService
|
|
).startWithLastProfile;
|
|
},
|
|
get() {
|
|
return this._getLaunchOnLoginEnabledValue;
|
|
},
|
|
setup(emitChange) {
|
|
if (AppConstants.platform !== "win") {
|
|
/**
|
|
* WindowsLaunchOnLogin isnt available if not on windows
|
|
* but this setup function still fires, so must prevent
|
|
* WindowsLaunchOnLogin.getLaunchOnLoginEnabled
|
|
* below from executing unnecessarily.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
/** @type {boolean} */
|
|
let getLaunchOnLoginEnabledValue;
|
|
let maybeEmitChange = () => {
|
|
if (
|
|
getLaunchOnLoginEnabledValue !== this._getLaunchOnLoginEnabledValue
|
|
) {
|
|
this._getLaunchOnLoginEnabledValue = getLaunchOnLoginEnabledValue;
|
|
emitChange();
|
|
}
|
|
};
|
|
if (!this.startWithLastProfile) {
|
|
getLaunchOnLoginEnabledValue = false;
|
|
maybeEmitChange();
|
|
} else {
|
|
// @ts-ignore bug 1996860
|
|
WindowsLaunchOnLogin.getLaunchOnLoginEnabled().then(val => {
|
|
getLaunchOnLoginEnabledValue = val;
|
|
maybeEmitChange();
|
|
});
|
|
}
|
|
},
|
|
visible: ({ windowsLaunchOnLoginEnabled }) => {
|
|
let isVisible =
|
|
AppConstants.platform === "win" && windowsLaunchOnLoginEnabled.value;
|
|
if (isVisible) {
|
|
// @ts-ignore bug 1996860
|
|
NimbusFeatures.windowsLaunchOnLogin.recordExposureEvent({
|
|
once: true,
|
|
});
|
|
}
|
|
return isVisible;
|
|
},
|
|
disabled({ launchOnLoginApproved }) {
|
|
return !this.startWithLastProfile || !launchOnLoginApproved.value;
|
|
},
|
|
onUserChange(checked) {
|
|
Glean.launchOnLogin.userToggle.record({ enabled: checked });
|
|
if (checked) {
|
|
// windowsLaunchOnLogin has been checked: create registry key or shortcut
|
|
// The shortcut is created with the same AUMID as Firefox itself. However,
|
|
// this is not set during browser tests and the fallback of checking the
|
|
// registry fails. As such we pass an arbitrary AUMID for the purpose
|
|
// of testing.
|
|
// @ts-ignore bug 1996860
|
|
WindowsLaunchOnLogin.createLaunchOnLogin();
|
|
Services.prefs.setBoolPref(
|
|
"browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
|
|
true
|
|
);
|
|
} else {
|
|
// windowsLaunchOnLogin has been unchecked: delete registry key and shortcut
|
|
// @ts-ignore bug 1996860
|
|
WindowsLaunchOnLogin.removeLaunchOnLogin();
|
|
}
|
|
},
|
|
})
|
|
);
|
|
|
|
Preferences.addSetting({
|
|
id: "windowsLaunchOnLoginDisabledProfileBox",
|
|
deps: ["windowsLaunchOnLoginEnabled"],
|
|
visible: ({ windowsLaunchOnLoginEnabled }) => {
|
|
if (AppConstants.platform !== "win") {
|
|
return false;
|
|
}
|
|
let startWithLastProfile = Cc[
|
|
"@mozilla.org/toolkit/profile-service;1"
|
|
].getService(Ci.nsIToolkitProfileService).startWithLastProfile;
|
|
|
|
return !startWithLastProfile && windowsLaunchOnLoginEnabled.value;
|
|
},
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "windowsLaunchOnLoginDisabledBox",
|
|
deps: ["launchOnLoginApproved", "windowsLaunchOnLoginEnabled"],
|
|
visible: ({ launchOnLoginApproved, windowsLaunchOnLoginEnabled }) => {
|
|
if (AppConstants.platform !== "win") {
|
|
return false;
|
|
}
|
|
let startWithLastProfile = Cc[
|
|
"@mozilla.org/toolkit/profile-service;1"
|
|
].getService(Ci.nsIToolkitProfileService).startWithLastProfile;
|
|
|
|
return (
|
|
startWithLastProfile &&
|
|
!launchOnLoginApproved.value &&
|
|
windowsLaunchOnLoginEnabled.value
|
|
);
|
|
},
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
/**
|
|
* The "Open previous windows and tabs" option on about:preferences page.
|
|
*/
|
|
id: "browserRestoreSession",
|
|
pref: "browser.startup.page",
|
|
deps: ["privateBrowsingAutoStart"],
|
|
get:
|
|
/**
|
|
* Returns the value of the "Open previous windows and tabs" option based
|
|
* on the value of the browser.privatebrowsing.autostart pref.
|
|
*
|
|
* @param {number | undefined} value
|
|
* @returns {boolean}
|
|
*/
|
|
value => {
|
|
const pbAutoStartPref = Preferences.get(
|
|
"browser.privatebrowsing.autostart"
|
|
);
|
|
let newValue = pbAutoStartPref.value
|
|
? false
|
|
: value === gMainPane.STARTUP_PREF_RESTORE_SESSION;
|
|
|
|
return newValue;
|
|
},
|
|
set: checked => {
|
|
const startupPref = Preferences.get("browser.startup.page");
|
|
let newValue;
|
|
|
|
if (checked) {
|
|
// We need to restore the blank homepage setting in our other pref
|
|
if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
|
|
// @ts-ignore bug 1996860
|
|
HomePage.safeSet("about:blank");
|
|
}
|
|
newValue = gMainPane.STARTUP_PREF_RESTORE_SESSION;
|
|
} else {
|
|
newValue = gMainPane.STARTUP_PREF_HOMEPAGE;
|
|
}
|
|
return newValue;
|
|
},
|
|
disabled: deps => {
|
|
return deps.privateBrowsingAutoStart.value;
|
|
},
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "sessionRestoreNewTab",
|
|
pref: "browser.sessionstore.newTabOnRestore",
|
|
deps: ["browserRestoreSession"],
|
|
visible: () =>
|
|
Services.prefs.getBoolPref(
|
|
"browser.sessionstore.newTabOnRestore.showSetting",
|
|
false
|
|
),
|
|
disabled: deps => !deps.browserRestoreSession.value,
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "containersPane",
|
|
onUserClick(e) {
|
|
e.preventDefault();
|
|
gotoPref("paneContainers2");
|
|
},
|
|
});
|
|
Preferences.addSetting({ id: "containersPlaceholder" });
|
|
Preferences.addSetting({
|
|
id: "legacyTranslationsVisible",
|
|
deps: ["aiControlDefault", "aiControlTranslations"],
|
|
visible: ({ aiControlDefault, aiControlTranslations }) =>
|
|
!Services.prefs.getBoolPref("browser.settings-redesign.enabled", false) &&
|
|
canShowAiFeature(aiControlTranslations, aiControlDefault),
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "connectionSettings",
|
|
onUserClick: () => gMainPane.showConnections(),
|
|
controllingExtensionInfo: {
|
|
storeId: PROXY_KEY,
|
|
l10nId: "extension-controlling-proxy-config",
|
|
allowControl: true,
|
|
},
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "alwaysCheckDefault",
|
|
pref: "browser.shell.checkDefaultBrowser",
|
|
setup: emitChange => {
|
|
if (!DefaultBrowserHelper.canCheck) {
|
|
return undefined;
|
|
}
|
|
return DefaultBrowserHelper.pollForDefaultChanges(emitChange);
|
|
},
|
|
/**
|
|
* Show button for setting browser as default browser or information that
|
|
* browser is already the default browser.
|
|
*/
|
|
visible: () => DefaultBrowserHelper.canCheck,
|
|
disabled: (_, setting) =>
|
|
!DefaultBrowserHelper.canCheck ||
|
|
setting.locked ||
|
|
DefaultBrowserHelper.isBrowserDefault,
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "isDefaultPane",
|
|
deps: ["alwaysCheckDefault"],
|
|
visible: () =>
|
|
DefaultBrowserHelper.canCheck &&
|
|
DefaultBrowserHelper.isBrowserDefault &&
|
|
Services.policies.isAllowed("setDefaultBrowser"),
|
|
});
|
|
|
|
Preferences.addSetting({
|
|
id: "isNotDefaultPane",
|
|
deps: ["alwaysCheckDefault"],
|
|
visible: () =>
|
|
DefaultBrowserHelper.canCheck &&
|
|
!DefaultBrowserHelper.isBrowserDefault &&
|
|
Services.policies.isAllowed("setDefaultBrowser"),
|
|
onUserClick: (e, { alwaysCheckDefault }) => {
|
|
if (!DefaultBrowserHelper.canCheck) {
|
|
return;
|
|
}
|
|
const setDefaultButton = /** @type {MozButton} */ (e.target);
|
|
|
|
if (!setDefaultButton) {
|
|
return;
|
|
}
|
|
if (setDefaultButton.disabled) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Disable the set default button, so that the user
|
|
* doesn't try to hit it again while browser is being set to default.
|
|
*/
|
|
setDefaultButton.disabled = true;
|
|
alwaysCheckDefault.value = true;
|
|
DefaultBrowserHelper.setDefaultBrowser().finally(() => {
|
|
setDefaultButton.disabled = false;
|
|
});
|
|
},
|
|
});
|
|
|
|
// AI Control pref settings
|
|
Preferences.addSetting({
|
|
id: "aiControlDefault",
|
|
pref: "browser.ai.control.default",
|
|
});
|
|
Preferences.addSetting({
|
|
id: "aiControlTranslations",
|
|
pref: "browser.ai.control.translations",
|
|
});
|
|
Preferences.addSetting({
|
|
id: "aiControlPdfjsAltText",
|
|
pref: "browser.ai.control.pdfjsAltText",
|
|
});
|
|
Preferences.addSetting({
|
|
id: "aiControlSmartTabGroups",
|
|
pref: "browser.ai.control.smartTabGroups",
|
|
});
|
|
Preferences.addSetting({
|
|
id: "aiControlLinkPreviews",
|
|
pref: "browser.ai.control.linkPreviewKeyPoints",
|
|
});
|
|
Preferences.addSetting({
|
|
id: "aiControlSidebarChatbot",
|
|
pref: "browser.ai.control.sidebarChatbot",
|
|
});
|
|
Preferences.addSetting({
|
|
id: "aiControlSmartWindow",
|
|
pref: "browser.ai.control.smartWindow",
|
|
});
|
|
|
|
function createDefaultBrowserConfig({
|
|
includeIsDefaultPane = true,
|
|
inProgress = false,
|
|
hiddenFromSearch = false,
|
|
} = {}) {
|
|
const isDefaultPane = {
|
|
id: "isDefaultPane",
|
|
l10nId: "is-default-browser-2",
|
|
control: "moz-promo",
|
|
controlAttrs: {
|
|
imagesrc: "chrome://global/skin/illustrations/kit-happy.svg",
|
|
imagedisplay: "cover",
|
|
},
|
|
};
|
|
|
|
const isNotDefaultPane = {
|
|
id: "isNotDefaultPane",
|
|
l10nId: "is-not-default-browser-2",
|
|
control: "moz-promo",
|
|
options: [
|
|
{
|
|
control: "moz-button",
|
|
l10nId: "set-as-my-default-browser-2",
|
|
id: "setDefaultButton",
|
|
slot: "actions",
|
|
|
|
controlAttrs: {
|
|
type: "primary",
|
|
},
|
|
},
|
|
],
|
|
controlAttrs: {
|
|
imagesrc: "chrome://global/skin/illustrations/kit-concerned.svg",
|
|
imagedisplay: "cover",
|
|
},
|
|
};
|
|
|
|
const items = includeIsDefaultPane
|
|
? [isDefaultPane, isNotDefaultPane]
|
|
: [isNotDefaultPane];
|
|
|
|
return {
|
|
l10nId: "home-default-browser-title",
|
|
headingLevel: 2,
|
|
items,
|
|
...(inProgress && { inProgress }),
|
|
...(hiddenFromSearch && { hiddenFromSearch }),
|
|
};
|
|
}
|
|
|
|
function createStartupConfig(hidden = false) {
|
|
return {
|
|
l10nId: "startup-group",
|
|
headingLevel: 2,
|
|
hidden,
|
|
items: [
|
|
{
|
|
id: "browserRestoreSession",
|
|
l10nId: "startup-restore-windows-and-tabs",
|
|
items: [
|
|
{
|
|
id: "sessionRestoreNewTab",
|
|
l10nId: "windows-launch-on-login-open-new-tab",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "windowsLaunchOnLogin",
|
|
l10nId: "windows-launch-on-login",
|
|
},
|
|
{
|
|
id: "windowsLaunchOnLoginDisabledBox",
|
|
control: "moz-message-bar",
|
|
controlAttrs: {
|
|
role: "status",
|
|
},
|
|
options: [
|
|
{
|
|
control: "span",
|
|
l10nId: "windows-launch-on-login-disabled",
|
|
slot: "message",
|
|
options: [
|
|
{
|
|
control: "a",
|
|
controlAttrs: {
|
|
"data-l10n-name": "startup-link",
|
|
href: "ms-settings:startupapps",
|
|
target: "_self",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "windowsLaunchOnLoginDisabledProfileBox",
|
|
control: "moz-message-bar",
|
|
l10nId: "startup-windows-launch-on-login-profile-disabled",
|
|
controlAttrs: {
|
|
role: "status",
|
|
},
|
|
},
|
|
{
|
|
id: "alwaysCheckDefault",
|
|
l10nId: "always-check-default",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
SettingGroupManager.registerGroups({
|
|
defaultBrowser: createDefaultBrowserConfig(),
|
|
startup: createStartupConfig(
|
|
Services.prefs.getBoolPref("browser.settings-redesign.enabled", false)
|
|
),
|
|
});
|
|
|
|
/**
|
|
* @param {string} id - ID of {@link SettingGroup} custom element.
|
|
*/
|
|
function initSettingGroup(id) {
|
|
/** @type {SettingGroup[]} */
|
|
let groups = document.querySelectorAll(`setting-group[groupid=${id}]`);
|
|
const config = SettingGroupManager.get(id);
|
|
for (let group of groups) {
|
|
if (group && config) {
|
|
let sectionEnabled = srdSectionEnabled(id);
|
|
|
|
if (
|
|
(sectionEnabled && group.hasAttribute("data-srd-migrated")) ||
|
|
(config.inProgress && !sectionEnabled)
|
|
) {
|
|
group.remove();
|
|
}
|
|
|
|
let legacySections = document.querySelectorAll(
|
|
`[data-srd-groupid=${id}]`
|
|
);
|
|
for (let section of legacySections) {
|
|
if (sectionEnabled) {
|
|
section.hidden = true;
|
|
section.removeAttribute("data-category");
|
|
section.setAttribute("data-hidden-from-search", "true");
|
|
}
|
|
}
|
|
group.config = config;
|
|
group.getSetting = Preferences.getSetting.bind(Preferences);
|
|
group.srdEnabled = srdSectionPrefs.all;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = {
|
|
// browser.startup.page values
|
|
STARTUP_PREF_BLANK: 0,
|
|
STARTUP_PREF_HOMEPAGE: 1,
|
|
STARTUP_PREF_RESTORE_SESSION: 3,
|
|
|
|
/**
|
|
* Initialization of gMainPane.
|
|
*/
|
|
init() {
|
|
/**
|
|
* @param {string} aId
|
|
* @param {string} aEventType
|
|
* @param {(ev: Event) => void} aCallback
|
|
*/
|
|
function setEventListener(aId, aEventType, aCallback) {
|
|
document
|
|
.getElementById(aId)
|
|
.addEventListener(aEventType, aCallback.bind(gMainPane));
|
|
}
|
|
|
|
this.displayUseSystemLocale();
|
|
|
|
if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
|
|
gMainPane.initPrimaryBrowserLanguageUI();
|
|
}
|
|
|
|
gMainPane.initTranslations();
|
|
|
|
// Initialize settings groups from the config object.
|
|
initSettingGroup("browserLayout");
|
|
initSettingGroup("appearance");
|
|
initSettingGroup("drm");
|
|
initSettingGroup("contrast");
|
|
initSettingGroup("zoom");
|
|
initSettingGroup("fonts");
|
|
initSettingGroup("browserLanguage");
|
|
initSettingGroup("websiteLanguage");
|
|
initSettingGroup("browsing");
|
|
initSettingGroup("keyboardAndScrolling");
|
|
initSettingGroup("motionAndLink");
|
|
initSettingGroup("updates");
|
|
initSettingGroup("translations");
|
|
initSettingGroup("spellCheck");
|
|
initSettingGroup("performance");
|
|
initSettingGroup("defaultBrowser");
|
|
initSettingGroup("startup");
|
|
initSettingGroup("importBrowserData");
|
|
initSettingGroup("tabs");
|
|
initSettingGroup("profiles");
|
|
initSettingGroup("profilePane");
|
|
|
|
setEventListener("manageBrowserLanguagesButton", "command", function () {
|
|
gMainPane.showBrowserLanguagesSubDialog({ search: false });
|
|
});
|
|
|
|
setEventListener("chooseLanguage", "command", gMainPane.showLanguages);
|
|
|
|
// Initilize Application section.
|
|
|
|
if (!srdSectionEnabled("applications")) {
|
|
AppFileHandler._init();
|
|
}
|
|
|
|
// Listen for window unload so we can remove our preference observers.
|
|
window.addEventListener("unload", this);
|
|
|
|
// Notify observers that the UI is now ready
|
|
Services.obs.notifyObservers(window, "main-pane-loaded");
|
|
this.setInitialized();
|
|
},
|
|
|
|
preInit() {
|
|
promiseLoadHandlersList = new Promise((resolve, reject) => {
|
|
window.addEventListener(
|
|
"pageshow",
|
|
async () => {
|
|
await this.initialized;
|
|
try {
|
|
if (!srdSectionEnabled("applications")) {
|
|
await AppFileHandler.preInit();
|
|
Services.obs.notifyObservers(window, "app-handler-loaded");
|
|
}
|
|
resolve();
|
|
} catch (ex) {
|
|
reject(ex);
|
|
}
|
|
},
|
|
{ once: true }
|
|
);
|
|
});
|
|
},
|
|
|
|
handleSubcategory(subcategory) {
|
|
if (Services.policies && !Services.policies.isAllowed("profileImport")) {
|
|
return false;
|
|
}
|
|
if (subcategory == "migrate") {
|
|
this.showMigrationWizardDialog();
|
|
return true;
|
|
}
|
|
|
|
if (subcategory == "migrate-autoclose") {
|
|
this.showMigrationWizardDialog({ closeTabWhenDone: true });
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
// CONTAINERS
|
|
|
|
/*
|
|
* preferences:
|
|
*
|
|
* privacy.userContext.enabled
|
|
* - true if containers is enabled
|
|
*/
|
|
|
|
async onGetStarted() {
|
|
if (!AppConstants.MOZ_DEV_EDITION) {
|
|
return;
|
|
}
|
|
const win = Services.wm.getMostRecentWindow("navigator:browser");
|
|
if (!win) {
|
|
return;
|
|
}
|
|
const user = await fxAccounts.getSignedInUser();
|
|
if (user) {
|
|
// We have a user, open Sync preferences in the same tab
|
|
win.openTrustedLinkIn("about:preferences#sync", "current");
|
|
return;
|
|
}
|
|
if (!(await FxAccounts.canConnectAccount())) {
|
|
return;
|
|
}
|
|
let url =
|
|
await FxAccounts.config.promiseConnectAccountURI("dev-edition-setup");
|
|
let accountsTab = win.gBrowser.addWebTab(url);
|
|
win.gBrowser.selectedTab = accountsTab;
|
|
},
|
|
|
|
// HOME PAGE
|
|
/*
|
|
* Preferences:
|
|
*
|
|
* browser.startup.page
|
|
* - what page(s) to show when the user starts the application, as an integer:
|
|
*
|
|
* 0: a blank page (DEPRECATED - this can be set via browser.startup.homepage)
|
|
* 1: the home page (as set by the browser.startup.homepage pref)
|
|
* 2: the last page the user visited (DEPRECATED)
|
|
* 3: windows and tabs from the last session (a.k.a. session restore)
|
|
*
|
|
* The deprecated option is not exposed in UI; however, if the user has it
|
|
* selected and doesn't change the UI for this preference, the deprecated
|
|
* option is preserved.
|
|
*/
|
|
|
|
/**
|
|
* Utility function to enable/disable the button specified by aButtonID based
|
|
* on the value of the Boolean preference specified by aPreferenceID.
|
|
*/
|
|
updateButtons(aButtonID, aPreferenceID) {
|
|
var button = document.getElementById(aButtonID);
|
|
var preference = Preferences.get(aPreferenceID);
|
|
button.disabled = !preference.value;
|
|
return undefined;
|
|
},
|
|
|
|
/**
|
|
* Initialize the translations view.
|
|
*/
|
|
async initTranslations() {
|
|
let legacyTranslationsVisible = Preferences.getSetting(
|
|
"legacyTranslationsVisible"
|
|
);
|
|
/**
|
|
* Which phase a language download is in.
|
|
*
|
|
* @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase
|
|
*/
|
|
|
|
let translationsGroup = document.getElementById("translationsGroup");
|
|
let setTranslationsGroupVisbility = () => {
|
|
// Immediately show the group so that the async load of the component does
|
|
// not cause the layout to jump. The group will be empty initially.
|
|
translationsGroup.hidden = !legacyTranslationsVisible.visible;
|
|
translationsGroup.classList.toggle(
|
|
"setting-hidden",
|
|
translationsGroup.hidden
|
|
);
|
|
};
|
|
setTranslationsGroupVisbility();
|
|
|
|
legacyTranslationsVisible.on("change", setTranslationsGroupVisbility);
|
|
window.addEventListener(
|
|
"unload",
|
|
() =>
|
|
legacyTranslationsVisible.off("change", setTranslationsGroupVisbility),
|
|
{ once: true }
|
|
);
|
|
|
|
class TranslationsState {
|
|
/**
|
|
* The fully initialized state.
|
|
*
|
|
* @param {object} supportedLanguages
|
|
* @param {Array<{ langTag: string, displayName: string}>} languageList
|
|
* @param {Map<string, DownloadPhase>} downloadPhases
|
|
*/
|
|
constructor(supportedLanguages, languageList, downloadPhases) {
|
|
this.supportedLanguages = supportedLanguages;
|
|
this.languageList = languageList;
|
|
this.downloadPhases = downloadPhases;
|
|
}
|
|
|
|
/**
|
|
* Handles all of the async initialization logic.
|
|
*/
|
|
static async create() {
|
|
const supportedLanguages =
|
|
await TranslationsParent.getSupportedLanguages();
|
|
const languageList =
|
|
TranslationsParent.getLanguageList(supportedLanguages);
|
|
const downloadPhases =
|
|
await TranslationsState.createDownloadPhases(languageList);
|
|
|
|
if (supportedLanguages.languagePairs.length === 0) {
|
|
throw new Error(
|
|
"The supported languages list was empty. RemoteSettings may not be available at the moment."
|
|
);
|
|
}
|
|
|
|
return new TranslationsState(
|
|
supportedLanguages,
|
|
languageList,
|
|
downloadPhases
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Determine the download phase of each language file.
|
|
*
|
|
* @param {Array<{ langTag: string, displayName: string}>} languageList
|
|
* @returns {Promise<Map<string, DownloadPhase>>} Map the language tag to whether it is downloaded.
|
|
*/
|
|
static async createDownloadPhases(languageList) {
|
|
const downloadPhases = new Map();
|
|
for (const { langTag } of languageList) {
|
|
downloadPhases.set(
|
|
langTag,
|
|
(await TranslationsParent.hasAllFilesForLanguage(langTag))
|
|
? "downloaded"
|
|
: "uninstalled"
|
|
);
|
|
}
|
|
return downloadPhases;
|
|
}
|
|
}
|
|
|
|
class TranslationsView {
|
|
/** @type {Map<string, XULButton>} */
|
|
deleteButtons = new Map();
|
|
/** @type {Map<string, XULButton>} */
|
|
downloadButtons = new Map();
|
|
|
|
/**
|
|
* @param {TranslationsState} state
|
|
*/
|
|
constructor(state) {
|
|
this.state = state;
|
|
this.elements = {
|
|
settingsButton: document.getElementById(
|
|
"translations-manage-settings-button"
|
|
),
|
|
installList: document.getElementById(
|
|
"translations-manage-install-list"
|
|
),
|
|
installAll: document.getElementById(
|
|
"translations-manage-install-all"
|
|
),
|
|
deleteAll: document.getElementById("translations-manage-delete-all"),
|
|
error: document.getElementById("translations-manage-error"),
|
|
};
|
|
this.setup();
|
|
}
|
|
|
|
setup() {
|
|
this.buildLanguageList();
|
|
|
|
this.elements.settingsButton.addEventListener(
|
|
"command",
|
|
gMainPane.showTranslationsSettings
|
|
);
|
|
this.elements.installAll.addEventListener(
|
|
"command",
|
|
this.handleInstallAll
|
|
);
|
|
this.elements.deleteAll.addEventListener(
|
|
"command",
|
|
this.handleDeleteAll
|
|
);
|
|
|
|
Services.obs.addObserver(this, "intl:app-locales-changed");
|
|
}
|
|
|
|
destroy() {
|
|
Services.obs.removeObserver(this, "intl:app-locales-changed");
|
|
}
|
|
|
|
handleInstallAll = async () => {
|
|
this.hideError();
|
|
this.disableButtons(true);
|
|
try {
|
|
await TranslationsParent.downloadAllFiles();
|
|
this.markAllDownloadPhases("downloaded");
|
|
} catch (error) {
|
|
TranslationsView.showError(
|
|
"translations-manage-error-download",
|
|
error
|
|
);
|
|
await this.reloadDownloadPhases();
|
|
this.updateAllButtons();
|
|
}
|
|
this.disableButtons(false);
|
|
};
|
|
|
|
handleDeleteAll = async () => {
|
|
this.hideError();
|
|
this.disableButtons(true);
|
|
try {
|
|
await TranslationsUtils.deleteAllLanguageFiles();
|
|
this.markAllDownloadPhases("uninstalled");
|
|
} catch (error) {
|
|
TranslationsView.showError("translations-manage-error-remove", error);
|
|
// The download phases are invalidated with the error and must be reloaded.
|
|
await this.reloadDownloadPhases();
|
|
console.error(error);
|
|
}
|
|
this.disableButtons(false);
|
|
};
|
|
|
|
/**
|
|
* @param {string} langTag
|
|
* @returns {Function}
|
|
*/
|
|
getDownloadButtonHandler(langTag) {
|
|
return async () => {
|
|
this.hideError();
|
|
this.updateDownloadPhase(langTag, "loading");
|
|
try {
|
|
await TranslationsParent.downloadLanguageFiles(langTag);
|
|
this.updateDownloadPhase(langTag, "downloaded");
|
|
} catch (error) {
|
|
TranslationsView.showError(
|
|
"translations-manage-error-download",
|
|
error
|
|
);
|
|
this.updateDownloadPhase(langTag, "uninstalled");
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} langTag
|
|
* @returns {Function}
|
|
*/
|
|
getDeleteButtonHandler(langTag) {
|
|
return async () => {
|
|
this.hideError();
|
|
this.updateDownloadPhase(langTag, "loading");
|
|
try {
|
|
await TranslationsParent.deleteLanguageFiles(langTag);
|
|
this.updateDownloadPhase(langTag, "uninstalled");
|
|
} catch (error) {
|
|
TranslationsView.showError(
|
|
"translations-manage-error-remove",
|
|
error
|
|
);
|
|
// The download phases are invalidated with the error and must be reloaded.
|
|
await this.reloadDownloadPhases();
|
|
}
|
|
};
|
|
}
|
|
|
|
buildLanguageList() {
|
|
const listFragment = document.createDocumentFragment();
|
|
|
|
for (const { langTag, displayName } of this.state.languageList) {
|
|
const hboxRow = document.createXULElement("hbox");
|
|
hboxRow.classList.add("translations-manage-language");
|
|
hboxRow.setAttribute("data-lang-tag", langTag);
|
|
|
|
const languageLabel = document.createXULElement("label");
|
|
languageLabel.textContent = displayName; // The display name is already localized.
|
|
|
|
const downloadButton = document.createXULElement("button");
|
|
const deleteButton = document.createXULElement("button");
|
|
|
|
downloadButton.addEventListener(
|
|
"command",
|
|
this.getDownloadButtonHandler(langTag)
|
|
);
|
|
deleteButton.addEventListener(
|
|
"command",
|
|
this.getDeleteButtonHandler(langTag)
|
|
);
|
|
|
|
document.l10n.setAttributes(
|
|
downloadButton,
|
|
"translations-manage-language-download-button"
|
|
);
|
|
document.l10n.setAttributes(
|
|
deleteButton,
|
|
"translations-manage-language-remove-button"
|
|
);
|
|
|
|
downloadButton.hidden = true;
|
|
deleteButton.hidden = true;
|
|
|
|
this.deleteButtons.set(langTag, deleteButton);
|
|
this.downloadButtons.set(langTag, downloadButton);
|
|
|
|
hboxRow.appendChild(languageLabel);
|
|
hboxRow.appendChild(downloadButton);
|
|
hboxRow.appendChild(deleteButton);
|
|
listFragment.appendChild(hboxRow);
|
|
}
|
|
this.updateAllButtons();
|
|
this.elements.installList.appendChild(listFragment);
|
|
}
|
|
|
|
/**
|
|
* Update the DownloadPhase for a single langTag.
|
|
*
|
|
* @param {string} langTag
|
|
* @param {DownloadPhase} downloadPhase
|
|
*/
|
|
updateDownloadPhase(langTag, downloadPhase) {
|
|
this.state.downloadPhases.set(langTag, downloadPhase);
|
|
this.updateButton(langTag, downloadPhase);
|
|
this.updateHeaderButtons();
|
|
}
|
|
|
|
/**
|
|
* Recreates the download map when the state is invalidated.
|
|
*/
|
|
async reloadDownloadPhases() {
|
|
this.state.downloadPhases =
|
|
await TranslationsState.createDownloadPhases(this.state.languageList);
|
|
this.updateAllButtons();
|
|
}
|
|
|
|
/**
|
|
* Set all the downloads.
|
|
*
|
|
* @param {DownloadPhase} downloadPhase
|
|
*/
|
|
markAllDownloadPhases(downloadPhase) {
|
|
const { downloadPhases } = this.state;
|
|
for (const key of downloadPhases.keys()) {
|
|
downloadPhases.set(key, downloadPhase);
|
|
}
|
|
this.updateAllButtons();
|
|
}
|
|
|
|
/**
|
|
* If all languages are downloaded, or no languages are downloaded then
|
|
* the visibility of the buttons need to change.
|
|
*/
|
|
updateHeaderButtons() {
|
|
let allDownloaded = true;
|
|
let allUninstalled = true;
|
|
for (const downloadPhase of this.state.downloadPhases.values()) {
|
|
if (downloadPhase === "loading") {
|
|
// Don't count loading towards this calculation.
|
|
continue;
|
|
}
|
|
allDownloaded &&= downloadPhase === "downloaded";
|
|
allUninstalled &&= downloadPhase === "uninstalled";
|
|
}
|
|
|
|
this.elements.installAll.hidden = allDownloaded;
|
|
this.elements.deleteAll.hidden = allUninstalled;
|
|
}
|
|
|
|
/**
|
|
* Update the buttons according to their download state.
|
|
*/
|
|
updateAllButtons() {
|
|
this.updateHeaderButtons();
|
|
for (const [langTag, downloadPhase] of this.state.downloadPhases) {
|
|
this.updateButton(langTag, downloadPhase);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} langTag
|
|
* @param {DownloadPhase} downloadPhase
|
|
*/
|
|
updateButton(langTag, downloadPhase) {
|
|
const downloadButton = this.downloadButtons.get(langTag);
|
|
const deleteButton = this.deleteButtons.get(langTag);
|
|
switch (downloadPhase) {
|
|
case "downloaded":
|
|
downloadButton.hidden = true;
|
|
deleteButton.hidden = false;
|
|
downloadButton.removeAttribute("disabled");
|
|
break;
|
|
case "uninstalled":
|
|
downloadButton.hidden = false;
|
|
deleteButton.hidden = true;
|
|
downloadButton.removeAttribute("disabled");
|
|
break;
|
|
case "loading":
|
|
downloadButton.hidden = false;
|
|
deleteButton.hidden = true;
|
|
downloadButton.setAttribute("disabled", "true");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} isDisabled
|
|
*/
|
|
disableButtons(isDisabled) {
|
|
this.elements.installAll.disabled = isDisabled;
|
|
this.elements.deleteAll.disabled = isDisabled;
|
|
for (const button of this.downloadButtons.values()) {
|
|
button.disabled = isDisabled;
|
|
}
|
|
for (const button of this.deleteButtons.values()) {
|
|
button.disabled = isDisabled;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is static in case an error happens during the creation of the
|
|
* TranslationsState.
|
|
*
|
|
* @param {string} l10nId
|
|
* @param {Error} error
|
|
*/
|
|
static showError(l10nId, error) {
|
|
console.error(error);
|
|
const errorMessage = document.getElementById(
|
|
"translations-manage-error"
|
|
);
|
|
errorMessage.hidden = false;
|
|
document.l10n.setAttributes(errorMessage, l10nId);
|
|
}
|
|
|
|
hideError() {
|
|
this.elements.error.hidden = true;
|
|
}
|
|
|
|
observe(_subject, topic, _data) {
|
|
if (topic === "intl:app-locales-changed") {
|
|
this.refreshLanguageListDisplay();
|
|
}
|
|
}
|
|
|
|
refreshLanguageListDisplay() {
|
|
try {
|
|
const languageDisplayNames =
|
|
TranslationsParent.createLanguageDisplayNames();
|
|
|
|
for (const row of this.elements.installList.children) {
|
|
const rowLangTag = row.getAttribute("data-lang-tag");
|
|
if (!rowLangTag) {
|
|
continue;
|
|
}
|
|
|
|
const label = row.querySelector("label");
|
|
if (label) {
|
|
const newDisplayName = languageDisplayNames.of(rowLangTag);
|
|
if (label.textContent !== newDisplayName) {
|
|
label.textContent = newDisplayName;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
TranslationsState.create().then(
|
|
state => {
|
|
this._translationsView = new TranslationsView(state);
|
|
},
|
|
error => {
|
|
// This error can happen when a user is not connected to the internet, or
|
|
// RemoteSettings is down for some reason.
|
|
TranslationsView.showError("translations-manage-error-list", error);
|
|
}
|
|
);
|
|
},
|
|
|
|
initPrimaryBrowserLanguageUI() {
|
|
// This will register the "command" listener.
|
|
let menulist = document.getElementById("primaryBrowserLocale");
|
|
new SelectionChangedMenulist(menulist, event => {
|
|
gMainPane.onPrimaryBrowserLanguageMenuChange(event);
|
|
});
|
|
|
|
gMainPane.updatePrimaryBrowserLanguageUI(Services.locale.appLocaleAsBCP47);
|
|
},
|
|
|
|
/**
|
|
* Update the available list of locales and select the locale that the user
|
|
* is "selecting". This could be the currently requested locale or a locale
|
|
* that the user would like to switch to after confirmation.
|
|
*
|
|
* @param {string} selected - The selected BCP 47 locale.
|
|
*/
|
|
async updatePrimaryBrowserLanguageUI(selected) {
|
|
let available = await LangPackMatcher.getAvailableLocales();
|
|
let localeNames = Services.intl.getLocaleDisplayNames(
|
|
undefined,
|
|
available,
|
|
{ preferNative: true }
|
|
);
|
|
let locales = available.map((code, i) => ({ code, name: localeNames[i] }));
|
|
locales.sort((a, b) => a.name > b.name);
|
|
|
|
let fragment = document.createDocumentFragment();
|
|
for (let { code, name } of locales) {
|
|
let menuitem = document.createXULElement("menuitem");
|
|
menuitem.setAttribute("value", code);
|
|
menuitem.setAttribute("label", name);
|
|
fragment.appendChild(menuitem);
|
|
}
|
|
|
|
// Add an option to search for more languages if downloading is supported.
|
|
if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) {
|
|
let menuitem = document.createXULElement("menuitem");
|
|
menuitem.id = "primaryBrowserLocaleSearch";
|
|
menuitem.setAttribute(
|
|
"label",
|
|
await document.l10n.formatValue("browser-languages-search")
|
|
);
|
|
menuitem.setAttribute("value", "search");
|
|
fragment.appendChild(menuitem);
|
|
}
|
|
|
|
let menulist = document.getElementById("primaryBrowserLocale");
|
|
let menupopup = menulist.querySelector("menupopup");
|
|
menupopup.textContent = "";
|
|
menupopup.appendChild(fragment);
|
|
menulist.value = selected;
|
|
|
|
document.getElementById("browserLanguagesBox").hidden = false;
|
|
},
|
|
|
|
/* Show the confirmation message bar to allow a restart into the new locales. */
|
|
async showConfirmLanguageChangeMessageBar(locales) {
|
|
let messageBar = document.getElementById("confirmBrowserLanguage");
|
|
|
|
// Get the bundle for the new locale.
|
|
let newBundle = getBundleForLocales(locales);
|
|
|
|
// Find the messages and labels.
|
|
let messages = await Promise.all(
|
|
[newBundle, document.l10n].map(async bundle =>
|
|
bundle.formatValue("confirm-browser-language-change-description")
|
|
)
|
|
);
|
|
let buttonLabels = await Promise.all(
|
|
[newBundle, document.l10n].map(async bundle =>
|
|
bundle.formatValue("confirm-browser-language-change-button")
|
|
)
|
|
);
|
|
|
|
// If both the message and label are the same, just include one row.
|
|
if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) {
|
|
messages.pop();
|
|
buttonLabels.pop();
|
|
}
|
|
|
|
let contentContainer = messageBar.querySelector(
|
|
".message-bar-content-container"
|
|
);
|
|
contentContainer.textContent = "";
|
|
|
|
for (let i = 0; i < messages.length; i++) {
|
|
let messageContainer = document.createXULElement("hbox");
|
|
messageContainer.classList.add("message-bar-content");
|
|
messageContainer.style.flex = "1 50%";
|
|
messageContainer.setAttribute("align", "center");
|
|
|
|
let description = document.createXULElement("description");
|
|
description.classList.add("message-bar-description");
|
|
|
|
if (i == 0 && Services.intl.getScriptDirection(locales[0]) === "rtl") {
|
|
description.classList.add("rtl-locale");
|
|
}
|
|
description.setAttribute("flex", "1");
|
|
description.textContent = messages[i];
|
|
messageContainer.appendChild(description);
|
|
|
|
let button = document.createXULElement("button");
|
|
button.addEventListener(
|
|
"command",
|
|
gMainPane.confirmBrowserLanguageChange
|
|
);
|
|
button.classList.add("message-bar-button");
|
|
button.setAttribute("locales", locales.join(","));
|
|
button.setAttribute("label", buttonLabels[i]);
|
|
messageContainer.appendChild(button);
|
|
|
|
contentContainer.appendChild(messageContainer);
|
|
}
|
|
|
|
messageBar.hidden = false;
|
|
gMainPane.selectedLocalesForRestart = locales;
|
|
},
|
|
|
|
hideConfirmLanguageChangeMessageBar() {
|
|
let messageBar = document.getElementById("confirmBrowserLanguage");
|
|
messageBar.hidden = true;
|
|
let contentContainer = messageBar.querySelector(
|
|
".message-bar-content-container"
|
|
);
|
|
contentContainer.textContent = "";
|
|
gMainPane.requestingLocales = null;
|
|
},
|
|
|
|
/* Confirm the locale change and restart the browser in the new locale. */
|
|
confirmBrowserLanguageChange(event) {
|
|
let localesString = (event.target.getAttribute("locales") || "").trim();
|
|
if (!localesString || !localesString.length) {
|
|
return;
|
|
}
|
|
let locales = localesString.split(",");
|
|
Multilingual.applyAndRestart(locales);
|
|
},
|
|
|
|
/* Show or hide the confirm change message bar based on the new locale. */
|
|
onPrimaryBrowserLanguageMenuChange(event) {
|
|
let locale = event.target.value;
|
|
|
|
if (locale == "search") {
|
|
gMainPane.showBrowserLanguagesSubDialog({ search: true });
|
|
return;
|
|
} else if (locale == Services.locale.appLocaleAsBCP47) {
|
|
this.hideConfirmLanguageChangeMessageBar();
|
|
return;
|
|
}
|
|
|
|
let newLocales = Array.from(
|
|
new Set([locale, ...Services.locale.requestedLocales]).values()
|
|
);
|
|
|
|
Multilingual.recordTelemetry("reorder");
|
|
|
|
switch (Multilingual.getTransitionType(newLocales)) {
|
|
case Multilingual.TransitionType.RestartRequired:
|
|
// Prepare to change the locales, as they were different.
|
|
gMainPane.showConfirmLanguageChangeMessageBar(newLocales);
|
|
gMainPane.updatePrimaryBrowserLanguageUI(newLocales[0]);
|
|
break;
|
|
case Multilingual.TransitionType.LiveReload:
|
|
Services.locale.requestedLocales = newLocales;
|
|
gMainPane.updatePrimaryBrowserLanguageUI(
|
|
Services.locale.appLocaleAsBCP47
|
|
);
|
|
gMainPane.hideConfirmLanguageChangeMessageBar();
|
|
break;
|
|
case Multilingual.TransitionType.LocalesMatch:
|
|
// They matched, so we can reset the UI.
|
|
gMainPane.updatePrimaryBrowserLanguageUI(
|
|
Services.locale.appLocaleAsBCP47
|
|
);
|
|
gMainPane.hideConfirmLanguageChangeMessageBar();
|
|
break;
|
|
default:
|
|
throw new Error("Unhandled transition type.");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Shows a window dialog containing the profile selector page.
|
|
*/
|
|
manageProfiles() {
|
|
const win = window.browsingContext.topChromeWindow;
|
|
|
|
win.toOpenWindowByType(
|
|
"about:profilemanager",
|
|
"about:profilemanager",
|
|
"chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar,centerscreen"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Shows a dialog in which the preferred language for web content may be set.
|
|
*/
|
|
showLanguages() {
|
|
gSubDialog.open(
|
|
"chrome://browser/content/preferences/dialogs/languages.xhtml"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Open the browser languages sub dialog in either the normal mode, or search mode.
|
|
* The search mode is only available from the menu to change the primary browser
|
|
* language.
|
|
*
|
|
* @param {{ search: boolean }} search
|
|
*/
|
|
showBrowserLanguagesSubDialog({ search }) {
|
|
// Record the telemetry event with an id to associate related actions.
|
|
let telemetryId = parseInt(
|
|
Services.telemetry.msSinceProcessStart(),
|
|
10
|
|
).toString();
|
|
let method = search ? "search" : "manage";
|
|
Multilingual.recordTelemetry(method, telemetryId);
|
|
|
|
let opts = {
|
|
selectedLocalesForRestart: gMainPane.selectedLocalesForRestart,
|
|
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() {
|
|
// When the subdialog is closed, settings are stored on gBrowserLanguagesDialog.
|
|
// The next time the dialog is opened, a new gBrowserLanguagesDialog is created.
|
|
let { selected } = this.gBrowserLanguagesDialog;
|
|
|
|
this.gBrowserLanguagesDialog.recordTelemetry(
|
|
selected ? "accept" : "cancel"
|
|
);
|
|
|
|
if (!selected) {
|
|
// No locales were selected. Cancel the operation.
|
|
return;
|
|
}
|
|
|
|
// Track how often locale fallback order is changed.
|
|
// Drop the first locale and filter to only include the overlapping set
|
|
const prevLocales = Services.locale.requestedLocales.filter(
|
|
lc => selected.indexOf(lc) > 0
|
|
);
|
|
const newLocales = selected.filter(
|
|
(lc, i) => i > 0 && prevLocales.includes(lc)
|
|
);
|
|
if (prevLocales.some((lc, i) => newLocales[i] != lc)) {
|
|
this.gBrowserLanguagesDialog.recordTelemetry("setFallback");
|
|
}
|
|
|
|
switch (Multilingual.getTransitionType(selected)) {
|
|
case Multilingual.TransitionType.RestartRequired:
|
|
gMainPane.showConfirmLanguageChangeMessageBar(selected);
|
|
gMainPane.updatePrimaryBrowserLanguageUI(selected[0]);
|
|
break;
|
|
case Multilingual.TransitionType.LiveReload:
|
|
Services.locale.requestedLocales = selected;
|
|
|
|
gMainPane.updatePrimaryBrowserLanguageUI(
|
|
Services.locale.appLocaleAsBCP47
|
|
);
|
|
gMainPane.hideConfirmLanguageChangeMessageBar();
|
|
break;
|
|
case Multilingual.TransitionType.LocalesMatch:
|
|
// They matched, so we can reset the UI.
|
|
gMainPane.updatePrimaryBrowserLanguageUI(
|
|
Services.locale.appLocaleAsBCP47
|
|
);
|
|
gMainPane.hideConfirmLanguageChangeMessageBar();
|
|
break;
|
|
default:
|
|
throw new Error("Unhandled transition type.");
|
|
}
|
|
},
|
|
|
|
displayUseSystemLocale() {
|
|
let appLocale = Services.locale.appLocaleAsBCP47;
|
|
let regionalPrefsLocales = Services.locale.regionalPrefsLocales;
|
|
if (!regionalPrefsLocales.length) {
|
|
return;
|
|
}
|
|
let systemLocale = regionalPrefsLocales[0];
|
|
let localeDisplayname = Services.intl.getLocaleDisplayNames(
|
|
undefined,
|
|
[systemLocale],
|
|
{ preferNative: true }
|
|
);
|
|
if (!localeDisplayname.length) {
|
|
return;
|
|
}
|
|
let localeName = localeDisplayname[0];
|
|
if (appLocale.split("-u-")[0] != systemLocale.split("-u-")[0]) {
|
|
let checkbox = document.getElementById("useSystemLocale");
|
|
document.l10n.setAttributes(checkbox, "use-system-locale", {
|
|
localeName,
|
|
});
|
|
checkbox.hidden = false;
|
|
}
|
|
},
|
|
|
|
showTranslationsSettings() {
|
|
gSubDialog.open(
|
|
"chrome://browser/content/preferences/dialogs/translations.xhtml"
|
|
);
|
|
},
|
|
|
|
// NETWORK
|
|
/**
|
|
* Displays a dialog in which proxy settings may be changed.
|
|
*/
|
|
showConnections() {
|
|
gSubDialog.open(
|
|
"chrome://browser/content/preferences/dialogs/connection.xhtml"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Displays the migration wizard dialog in an HTML dialog.
|
|
*/
|
|
async showMigrationWizardDialog({ closeTabWhenDone = false } = {}) {
|
|
let migrationWizardDialog = document.getElementById(
|
|
"migrationWizardDialog"
|
|
);
|
|
|
|
if (migrationWizardDialog.open) {
|
|
return;
|
|
}
|
|
|
|
await customElements.whenDefined("migration-wizard");
|
|
|
|
// Only create the migration-wizard once.
|
|
if (!migrationWizardDialog.firstElementChild) {
|
|
let wizard = document.createElement("migration-wizard");
|
|
wizard.toggleAttribute("dialog-mode", true);
|
|
migrationWizardDialog.appendChild(wizard);
|
|
migrationWizardDialog.addEventListener(
|
|
"MigrationWizard:Close",
|
|
function (e) {
|
|
e.currentTarget.close();
|
|
}
|
|
);
|
|
}
|
|
migrationWizardDialog.firstElementChild.requestState();
|
|
|
|
migrationWizardDialog.addEventListener(
|
|
"close",
|
|
() => {
|
|
// Let others know that the wizard is closed -- potentially because of a
|
|
// user action within the dialog that dispatches "MigrationWizard:Close"
|
|
// but this also covers cases like hitting Escape.
|
|
Services.obs.notifyObservers(
|
|
migrationWizardDialog,
|
|
"MigrationWizard:Closed"
|
|
);
|
|
if (closeTabWhenDone) {
|
|
window.close();
|
|
}
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
migrationWizardDialog.showModal();
|
|
},
|
|
|
|
destroy() {
|
|
window.removeEventListener("unload", this);
|
|
|
|
// Clean up the TranslationsView instance if it exists
|
|
if (this._translationsView) {
|
|
this._translationsView.destroy();
|
|
this._translationsView = null;
|
|
}
|
|
},
|
|
|
|
// nsISupports
|
|
|
|
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
|
|
|
|
// nsIObserver
|
|
|
|
async observe(_, aTopic, aData) {
|
|
if (aTopic == "nsPref:changed") {
|
|
if (aData == PREF_CONTAINERS_EXTENSION) {
|
|
return;
|
|
}
|
|
// Rebuild the list when there are changes to preferences that influence
|
|
// whether or not to show certain entries in the list.
|
|
if (
|
|
!srdSectionEnabled("applications") &&
|
|
!AppFileHandler._storingAction
|
|
) {
|
|
await AppFileHandler._rebuildView();
|
|
}
|
|
}
|
|
},
|
|
|
|
// EventListener
|
|
|
|
handleEvent(aEvent) {
|
|
if (aEvent.type == "unload") {
|
|
this.destroy();
|
|
if (AppConstants.MOZ_UPDATER) {
|
|
onUnload();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Whether or not the given handler app is valid.
|
|
*
|
|
* @todo remove this method and use {@link AppFileHandler.isValidHandlerApp} when #appList dialog is no longer using it.
|
|
* @param aHandlerApp {nsIHandlerApp} the handler app in question
|
|
* @returns {boolean} whether or not it's valid
|
|
*/
|
|
isValidHandlerApp(aHandlerApp) {
|
|
return AppFileHandler.isValidHandlerApp(aHandlerApp);
|
|
},
|
|
|
|
/**
|
|
* @todo remove this method and use {@link AppFileHandler._getIconURLForHandlerApp} when #appList dialog is no longer using it.
|
|
* @param aHandlerApp {nsIHandlerApp}
|
|
* @returns {string}
|
|
*/
|
|
_getIconURLForHandlerApp(aHandlerApp) {
|
|
return getIconURLForHandlerApp(aHandlerApp);
|
|
},
|
|
};
|
|
gMainPane.initialized = new Promise(res => {
|
|
gMainPane.setInitialized = res;
|
|
});
|
|
|
|
var {
|
|
InternalHandlerInfoWrapper,
|
|
getFileDisplayName,
|
|
getLocalHandlerApp,
|
|
HandlerServiceHelpers,
|
|
getIconURLForHandlerApp,
|
|
} = ChromeUtils.importESModule(
|
|
"chrome://browser/content/preferences/config/downloads.mjs",
|
|
{ global: "current" }
|
|
);
|
|
|
|
/**
|
|
* @typedef {MozOption & {
|
|
* handlerApp: nsIHandlerApp | null | void
|
|
* }} ApplicationFileHandlerItemActionsMenuOption
|
|
*/
|
|
|
|
let gHandlerListItemFragment = window.MozXULElement.parseXULToFragment(`
|
|
<richlistitem>
|
|
<hbox class="typeContainer" flex="1" align="center">
|
|
<html:img class="typeIcon" width="16" height="16" />
|
|
<label class="typeDescription" flex="1" crop="end"/>
|
|
</hbox>
|
|
<hbox class="actionContainer" flex="1" align="center">
|
|
<html:img 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" aria-labelledby="actionColumn">
|
|
<menupopup/>
|
|
</menulist>
|
|
</hbox>
|
|
</richlistitem>
|
|
`);
|
|
|
|
/**
|
|
* This is associated to <richlistitem> elements in the handlers view.
|
|
* Maintained to support the legacy "Applications" section.
|
|
*/
|
|
class HandlerListItem {
|
|
/**
|
|
* @param {Node} node
|
|
* @returns {Node | undefined}
|
|
*/
|
|
static forNode(node) {
|
|
return gNodeToObjectMap.get(node);
|
|
}
|
|
|
|
/**
|
|
* @param {HandlerInfoWrapper} handlerInfoWrapper
|
|
*/
|
|
constructor(handlerInfoWrapper) {
|
|
this.handlerInfoWrapper = handlerInfoWrapper;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Array<[string, string, string]>} iterable
|
|
*/
|
|
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 =>
|
|
AppFileHandler.onSelectAction(event.originalTarget)
|
|
);
|
|
|
|
let typeDescription = this.handlerInfoWrapper.typeDescription;
|
|
this.setOrRemoveAttributes([
|
|
[null, "type", this.handlerInfoWrapper.type],
|
|
[".typeIcon", "srcset", this.handlerInfoWrapper.iconSrcSet],
|
|
]);
|
|
localizeElement(
|
|
this.node.querySelector(".typeDescription"),
|
|
typeDescription
|
|
);
|
|
this.showActionsMenu = false;
|
|
}
|
|
|
|
refreshAction() {
|
|
let { actionIconClass } = this.handlerInfoWrapper;
|
|
this.setOrRemoveAttributes([
|
|
[null, APP_ICON_ATTR_NAME, actionIconClass],
|
|
[
|
|
".actionIcon",
|
|
"srcset",
|
|
actionIconClass ? null : this.handlerInfoWrapper.actionIconSrcset,
|
|
],
|
|
]);
|
|
const selectedItem = this.node.querySelector("[selected=true]");
|
|
if (!selectedItem) {
|
|
console.error("No selected item for " + this.handlerInfoWrapper.type);
|
|
return;
|
|
}
|
|
const { id, args } = document.l10n.getAttributes(selectedItem);
|
|
const messageIDs = {
|
|
"applications-action-save": "applications-action-save-label",
|
|
"applications-always-ask": "applications-always-ask-label",
|
|
"applications-open-inapp": "applications-open-inapp-label",
|
|
"applications-use-app-default": "applications-use-app-default-label",
|
|
"applications-use-app": "applications-use-app-label",
|
|
"applications-use-os-default": "applications-use-os-default-label",
|
|
"applications-use-other": "applications-use-other-label",
|
|
};
|
|
localizeElement(this.node.querySelector(".actionDescription"), {
|
|
id: messageIDs[id],
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler class for the legacy "Applications" section of settings. This can be
|
|
* removed when we ship the redesigned settings page.
|
|
*/
|
|
let AppFileHandler = (function () {
|
|
return new (class Handler {
|
|
/**
|
|
* The set of types the app knows how to handle. A hash of HandlerInfoWrapper
|
|
* objects, indexed by type.
|
|
*
|
|
* @type {Record<string, any>}
|
|
*/
|
|
_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.
|
|
*
|
|
* @type {Array<any>}
|
|
*/
|
|
_visibleTypes = [];
|
|
|
|
/**
|
|
* @type {HandlerListItem | null}
|
|
*/
|
|
selectedHandlerListItem = null;
|
|
|
|
// Sorting & Filtering
|
|
|
|
/**
|
|
* @type {Element | null}
|
|
*/
|
|
_sortColumn = null;
|
|
|
|
/**
|
|
* Currently-showing handler items.
|
|
*
|
|
* @type {Array<ApplicationListItem>}
|
|
*/
|
|
items = [];
|
|
|
|
get _list() {
|
|
return document.getElementById("handlersView");
|
|
}
|
|
|
|
get _filter() {
|
|
return document.getElementById("filter");
|
|
}
|
|
|
|
initialized = false;
|
|
|
|
async preInit() {
|
|
this._initListEventHandlers();
|
|
this._loadInternalHandlers();
|
|
this._loadApplicationHandlers();
|
|
|
|
await this._rebuildVisibleTypes();
|
|
await this._rebuildView();
|
|
await this._sortListView();
|
|
}
|
|
|
|
_init() {
|
|
setEventListener("filter", "MozInputSearch:search", () => this.filter());
|
|
setEventListener("typeColumn", "click", e => this.sort(e));
|
|
setEventListener("actionColumn", "click", e => this.sort(e));
|
|
|
|
// 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");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
_loadApplicationHandlers() {
|
|
HandlerServiceHelpers.loadApplicationHandlers(this._handledTypes);
|
|
}
|
|
|
|
_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;
|
|
}
|
|
});
|
|
}
|
|
|
|
_loadInternalHandlers() {
|
|
HandlerServiceHelpers.loadInternalHandlers(this._handledTypes);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sort the list when the user clicks on a column header.
|
|
*
|
|
* @param {CustomEvent} event
|
|
*/
|
|
sort(event) {
|
|
if (event.button != 0) {
|
|
return;
|
|
}
|
|
var column = event.target;
|
|
|
|
// If the user clicked on a new sort column, remove the direction indicator
|
|
// from the old column.
|
|
if (this._sortColumn && this._sortColumn != column) {
|
|
this._sortColumn.removeAttribute("sortDirection");
|
|
}
|
|
|
|
this._sortColumn = column;
|
|
|
|
// Set (or switch) the sort direction indicator.
|
|
if (column.getAttribute("sortDirection") == "ascending") {
|
|
column.setAttribute("sortDirection", "descending");
|
|
} else {
|
|
column.setAttribute("sortDirection", "ascending");
|
|
}
|
|
|
|
this._sortListView();
|
|
}
|
|
|
|
async _sortListView() {
|
|
if (!this._sortColumn) {
|
|
return;
|
|
}
|
|
let comp = new Services.intl.Collator(undefined, {
|
|
usage: "sort",
|
|
});
|
|
|
|
await document.l10n.translateFragment(this._list);
|
|
let items = Array.from(this._list.children);
|
|
|
|
let textForNode;
|
|
if (this._sortColumn.getAttribute("value") === "type") {
|
|
textForNode = n => n.querySelector(".typeDescription").textContent;
|
|
} else {
|
|
textForNode = n =>
|
|
n.querySelector(".actionsMenu").getAttribute("label");
|
|
}
|
|
|
|
let sortDir = this._sortColumn.getAttribute("sortDirection");
|
|
let multiplier = sortDir == "descending" ? -1 : 1;
|
|
items.sort(
|
|
(a, b) => multiplier * comp.compare(textForNode(a), textForNode(b))
|
|
);
|
|
|
|
// Re-append items in the correct order:
|
|
items.forEach(item => this._list.appendChild(item));
|
|
}
|
|
|
|
_filterView(frag = this._list) {
|
|
const filterValue = this._filter.value.toLowerCase();
|
|
for (let elem of frag.children) {
|
|
const typeDescription =
|
|
elem.querySelector(".typeDescription").textContent;
|
|
const actionDescription = elem
|
|
.querySelector(".actionDescription")
|
|
.getAttribute("value");
|
|
elem.hidden =
|
|
!typeDescription.toLowerCase().includes(filterValue) &&
|
|
!actionDescription.toLowerCase().includes(filterValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the header item.
|
|
*
|
|
* @return {MozBoxItem}
|
|
*/
|
|
_buildHeader() {
|
|
const headerElement = /** @type {MozBoxItem} */ (
|
|
document.createElement("moz-box-item")
|
|
);
|
|
headerElement.slot = "header";
|
|
this.typeColumn = document.createElement("label");
|
|
this.typeColumn.setAttribute("data-l10n-id", "applications-type-heading");
|
|
headerElement.appendChild(this.typeColumn);
|
|
|
|
this.actionColumn = document.createElement("label");
|
|
this.actionColumn.slot = "actions";
|
|
this.actionColumn.setAttribute(
|
|
"data-l10n-id",
|
|
"applications-action-heading"
|
|
);
|
|
headerElement.appendChild(this.actionColumn);
|
|
|
|
return headerElement;
|
|
}
|
|
|
|
/**
|
|
* Sorts the items alphabetically by their label.
|
|
*
|
|
* @param {Array<ApplicationFileHandlerItemActionsMenuOption>} unorderedItems
|
|
* @returns {Array<ApplicationFileHandlerItemActionsMenuOption>}
|
|
*/
|
|
_sortItems(unorderedItems) {
|
|
let comp = new Services.intl.Collator(undefined, {
|
|
usage: "sort",
|
|
});
|
|
const textForNode = item => item.getAttribute("label");
|
|
let multiplier = 1;
|
|
return unorderedItems.sort(
|
|
(a, b) => multiplier * comp.compare(textForNode(a), textForNode(b))
|
|
);
|
|
}
|
|
|
|
filter() {
|
|
this._rebuildView();
|
|
}
|
|
|
|
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.browsingContext, winTitle, Ci.nsIFilePicker.modeOpen);
|
|
fp.appendFilters(Ci.nsIFilePicker.filterApps);
|
|
fp.open(fpCallback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rebuild the actions menu for the selected entry. Gets called by
|
|
* the richlistitem constructor when an entry in the list gets selected.
|
|
*/
|
|
rebuildActionsMenu(
|
|
typeItem = this._list.selectedItem,
|
|
handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper
|
|
) {
|
|
var menu = typeItem.querySelector(".actionsMenu");
|
|
var menuPopup = menu.menupopup;
|
|
|
|
// Clear out existing items.
|
|
while (menuPopup.hasChildNodes()) {
|
|
menuPopup.removeChild(menuPopup.lastChild);
|
|
}
|
|
|
|
let internalMenuItem;
|
|
// Add the "Open in Firefox" option for optional internal handlers.
|
|
if (
|
|
handlerInfo instanceof InternalHandlerInfoWrapper &&
|
|
!handlerInfo.preventInternalViewing
|
|
) {
|
|
internalMenuItem = document.createXULElement("menuitem");
|
|
internalMenuItem.setAttribute(
|
|
"action",
|
|
Ci.nsIHandlerInfo.handleInternally
|
|
);
|
|
internalMenuItem.className = "menuitem-iconic";
|
|
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);
|
|
askMenuItem.className = "menuitem-iconic";
|
|
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");
|
|
saveMenuItem.className = "menuitem-iconic";
|
|
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,
|
|
}
|
|
);
|
|
let image = handlerInfo.iconURLForSystemDefault;
|
|
if (image) {
|
|
defaultMenuItem.setAttribute("image", image);
|
|
}
|
|
}
|
|
|
|
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,
|
|
});
|
|
let image = getIconURLForHandlerApp(possibleApp);
|
|
if (image) {
|
|
menuItem.setAttribute("image", image);
|
|
}
|
|
|
|
// Attach the handler app object to the menu item so we can use it
|
|
// to make changes to the datastore when the user selects the item.
|
|
menuItem.handlerApp = possibleApp;
|
|
|
|
menuPopup.appendChild(menuItem);
|
|
possibleAppMenuItems.push(menuItem);
|
|
}
|
|
// Add gio handlers
|
|
if (gGIOService) {
|
|
var gioApps = gGIOService.getAppsForURIScheme(handlerInfo.type);
|
|
let possibleHandlers = handlerInfo.possibleApplicationHandlers;
|
|
for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) {
|
|
// OS handler share the same name, it's most likely the same app, skipping...
|
|
if (handler.name == handlerInfo.defaultDescription) {
|
|
continue;
|
|
}
|
|
// Check if the handler is already in possibleHandlers
|
|
let appAlreadyInHandlers = false;
|
|
for (let i = possibleHandlers.length - 1; i >= 0; --i) {
|
|
let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
|
|
// nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp
|
|
if (handler.equals(app)) {
|
|
appAlreadyInHandlers = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!appAlreadyInHandlers) {
|
|
let menuItem = document.createXULElement("menuitem");
|
|
menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
|
|
document.l10n.setAttributes(menuItem, "applications-use-app", {
|
|
"app-name": handler.name,
|
|
});
|
|
|
|
let image = getIconURLForHandlerApp(handler);
|
|
if (image) {
|
|
menuItem.setAttribute("image", image);
|
|
}
|
|
|
|
// 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) {
|
|
AppFileHandler.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) {
|
|
AppFileHandler.manageApp(e);
|
|
});
|
|
document.l10n.setAttributes(menuItem, "applications-manage-app");
|
|
menuPopup.appendChild(menuItem);
|
|
}
|
|
|
|
// Select the item corresponding to the preferred action. If the always
|
|
// ask flag is set, it overrides the preferred action. Otherwise we pick
|
|
// the item identified by the preferred action (when the preferred action
|
|
// is to use a helper app, we have to pick the specific helper app item).
|
|
if (handlerInfo.alwaysAskBeforeHandling) {
|
|
menu.selectedItem = askMenuItem;
|
|
} else {
|
|
// The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
|
|
// the actions the application can take with content of various types.
|
|
// But since we've stopped support for plugins, there's no value
|
|
// identifying the "use plugin" action, so we use this constant instead.
|
|
const kActionUsePlugin = 5;
|
|
|
|
switch (handlerInfo.preferredAction) {
|
|
case Ci.nsIHandlerInfo.handleInternally:
|
|
if (internalMenuItem) {
|
|
menu.selectedItem = internalMenuItem;
|
|
} else {
|
|
console.error("No menu item defined to set!");
|
|
}
|
|
break;
|
|
case Ci.nsIHandlerInfo.useSystemDefault:
|
|
// We might not have a default item if we're not aware of an
|
|
// OS-default handler for this type:
|
|
menu.selectedItem = defaultMenuItem || askMenuItem;
|
|
break;
|
|
case Ci.nsIHandlerInfo.useHelperApp:
|
|
if (preferredApp) {
|
|
let preferredItem = possibleAppMenuItems.find(v =>
|
|
v.handlerApp.equals(preferredApp)
|
|
);
|
|
if (preferredItem) {
|
|
menu.selectedItem = preferredItem;
|
|
} else {
|
|
// This shouldn't happen, but let's make sure we end up with a
|
|
// selected item:
|
|
let possible = possibleAppMenuItems
|
|
.map(v => v.handlerApp && v.handlerApp.name)
|
|
.join(", ");
|
|
console.error(
|
|
new Error(
|
|
`Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})`
|
|
)
|
|
);
|
|
menu.selectedItem = askMenuItem;
|
|
}
|
|
}
|
|
break;
|
|
case kActionUsePlugin:
|
|
// We no longer support plugins, select "ask" instead:
|
|
menu.selectedItem = askMenuItem;
|
|
break;
|
|
case Ci.nsIHandlerInfo.saveToDisk:
|
|
menu.selectedItem = saveMenuItem;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
if (aHandlerApp instanceof Ci.nsIGIOHandlerApp) {
|
|
return aHandlerApp.id;
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
})();
|
|
})();
|