summaryrefslogtreecommitdiffstats
path: root/toolkit/components/printing/content/print.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/printing/content/print.js')
-rw-r--r--toolkit/components/printing/content/print.js2847
1 files changed, 2847 insertions, 0 deletions
diff --git a/toolkit/components/printing/content/print.js b/toolkit/components/printing/content/print.js
new file mode 100644
index 0000000000..d08d790522
--- /dev/null
+++ b/toolkit/components/printing/content/print.js
@@ -0,0 +1,2847 @@
+/* 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/. */
+
+const { PrintUtils, Services, AppConstants } =
+ window.docShell.chromeEventHandler.ownerGlobal;
+
+ChromeUtils.defineESModuleGetters(this, {
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
+});
+
+const PDF_JS_URI = "resource://pdf.js/web/viewer.html";
+const INPUT_DELAY_MS = Cu.isInAutomation ? 100 : 500;
+const MM_PER_POINT = 25.4 / 72;
+const INCHES_PER_POINT = 1 / 72;
+const INCHES_PER_MM = 1 / 25.4;
+const ourBrowser = window.docShell.chromeEventHandler;
+const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+);
+
+var logger = (function () {
+ const getMaxLogLevel = () =>
+ Services.prefs.getBoolPref("print.debug", false) ? "all" : "warn";
+
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+ let _logger = new ConsoleAPI({
+ prefix: "printUI",
+ maxLogLevel: getMaxLogLevel(),
+ });
+
+ function onPrefChange() {
+ if (_logger) {
+ _logger.maxLogLevel = getMaxLogLevel();
+ }
+ }
+ // Watch for pref changes and the maxLogLevel for the logger
+ Services.prefs.addObserver("print.debug", onPrefChange);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("print.debug", onPrefChange);
+ });
+ return _logger;
+})();
+
+function serializeSettings(settings, logPrefix) {
+ let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception?
+ let types = new Set(["string", "boolean", "number", "undefined"]);
+ let nameValues = {};
+ for (let key in settings) {
+ try {
+ if (!re.test(key) && types.has(typeof settings[key])) {
+ nameValues[key] = settings[key];
+ }
+ } catch (e) {
+ logger.warn("Exception accessing setting: ", key, e);
+ }
+ }
+ return JSON.stringify(nameValues, null, 2);
+}
+
+let printPending = false;
+let deferredTasks = [];
+function createDeferredTask(fn, timeout) {
+ let task = new DeferredTask(fn, timeout);
+ deferredTasks.push(task);
+ return task;
+}
+
+function cancelDeferredTasks() {
+ for (let task of deferredTasks) {
+ task.disarm();
+ }
+ PrintEventHandler._updatePrintPreviewTask?.disarm();
+ deferredTasks = [];
+}
+
+document.addEventListener(
+ "DOMContentLoaded",
+ e => {
+ window._initialized = PrintEventHandler.init().catch(e => console.error(e));
+ ourBrowser.setAttribute("flex", "0");
+ ourBrowser.setAttribute("constrainpopups", "false");
+ ourBrowser.classList.add("printSettingsBrowser");
+ ourBrowser.closest(".dialogBox")?.classList.add("printDialogBox");
+ },
+ { once: true }
+);
+
+window.addEventListener("dialogclosing", () => {
+ cancelDeferredTasks();
+});
+
+window.addEventListener(
+ "unload",
+ e => {
+ document.textContent = "";
+ },
+ { once: true }
+);
+
+var PrintEventHandler = {
+ settings: null,
+ defaultSettings: null,
+ allPaperSizes: {},
+ previewIsEmpty: false,
+ _delayedChanges: {},
+ _userChangedSettings: {},
+ settingFlags: {
+ margins: Ci.nsIPrintSettings.kInitSaveMargins,
+ customMargins: Ci.nsIPrintSettings.kInitSaveMargins,
+ orientation: Ci.nsIPrintSettings.kInitSaveOrientation,
+ paperId:
+ Ci.nsIPrintSettings.kInitSavePaperSize |
+ Ci.nsIPrintSettings.kInitSaveUnwriteableMargins,
+ printInColor: Ci.nsIPrintSettings.kInitSaveInColor,
+ scaling: Ci.nsIPrintSettings.kInitSaveScaling,
+ shrinkToFit: Ci.nsIPrintSettings.kInitSaveShrinkToFit,
+ printDuplex: Ci.nsIPrintSettings.kInitSaveDuplex,
+ printFootersHeaders:
+ Ci.nsIPrintSettings.kInitSaveHeaderLeft |
+ Ci.nsIPrintSettings.kInitSaveHeaderCenter |
+ Ci.nsIPrintSettings.kInitSaveHeaderRight |
+ Ci.nsIPrintSettings.kInitSaveFooterLeft |
+ Ci.nsIPrintSettings.kInitSaveFooterCenter |
+ Ci.nsIPrintSettings.kInitSaveFooterRight,
+ printBackgrounds:
+ Ci.nsIPrintSettings.kInitSaveBGColors |
+ Ci.nsIPrintSettings.kInitSaveBGImages,
+ },
+
+ topContentTitle: null,
+ topCurrentURI: null,
+ activeContentTitle: null,
+ activeCurrentURI: null,
+
+ get activeURI() {
+ return this.viewSettings.sourceVersion == "selection"
+ ? this.activeCurrentURI
+ : this.topCurrentURI;
+ },
+ get activeTitle() {
+ return this.viewSettings.sourceVersion == "selection"
+ ? this.activeContentTitle
+ : this.topContentTitle;
+ },
+
+ // These settings do not have an associated pref value or flag, but
+ // changing them requires us to update the print preview.
+ _nonFlaggedUpdatePreviewSettings: new Set([
+ "pageRanges",
+ "numPagesPerSheet",
+ "sourceVersion",
+ ]),
+ _noPreviewUpdateSettings: new Set(["numCopies", "printDuplex"]),
+
+ async init() {
+ Services.telemetry.scalarAdd("printing.preview_opened_tm", 1);
+
+ this.printPreviewEl =
+ ourBrowser.parentElement.querySelector("print-preview");
+
+ // Do not keep a reference to source browser, it may mutate after printing
+ // is initiated and the print preview clone must be a snapshot from the
+ // time that the print was started.
+ let sourceBrowsingContext = this.printPreviewEl.getSourceBrowsingContext();
+
+ let args = window.arguments[0];
+ this.printFrameOnly = args.getProperty("printFrameOnly");
+ this.printSelectionOnly = args.getProperty("printSelectionOnly");
+ this.isArticle = args.getProperty("isArticle");
+ this.hasSelection = await PrintUtils.checkForSelection(
+ sourceBrowsingContext
+ );
+
+ let sourcePrincipal =
+ sourceBrowsingContext.currentWindowGlobal.documentPrincipal;
+ let sourceIsPdf =
+ !sourcePrincipal.isNullPrincipal && sourcePrincipal.spec == PDF_JS_URI;
+ this.activeContentTitle =
+ sourceBrowsingContext.currentWindowContext.documentTitle;
+ this.activeCurrentURI =
+ sourceBrowsingContext.currentWindowContext.documentURI.spec;
+ let topWindowContext = sourceBrowsingContext.top.currentWindowContext;
+ this.topContentTitle = topWindowContext.documentTitle;
+ this.topCurrentURI = topWindowContext.documentURI.spec;
+ this.isReader = this.topCurrentURI.startsWith("about:reader");
+
+ let canSimplify = !this.isReader && this.isArticle;
+ if (!this.hasSelection && !canSimplify) {
+ document.getElementById("source-version-section").hidden = true;
+ } else {
+ document.getElementById("source-version-selection").hidden =
+ !this.hasSelection;
+ document.getElementById("source-version-simplified").hidden =
+ !canSimplify;
+ }
+
+ // We don't need the sourceBrowsingContext anymore, get rid of it.
+ sourceBrowsingContext = undefined;
+
+ this.printProgressIndicator = document.getElementById("print-progress");
+ this.printForm = document.getElementById("print");
+ if (sourceIsPdf) {
+ this.printForm.removeNonPdfSettings();
+ }
+
+ // Let the dialog appear before doing any potential main thread work.
+ await ourBrowser._dialogReady;
+
+ // First check the available destinations to ensure we get settings for an
+ // accessible printer.
+ let destinations,
+ defaultSystemPrinter,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName;
+ try {
+ ({
+ destinations,
+ defaultSystemPrinter,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName,
+ } = await this.getPrintDestinations());
+ } catch (e) {
+ this.reportPrintingError("PRINT_DESTINATIONS");
+ throw e;
+ }
+ PrintSettingsViewProxy.availablePrinters = printersByName;
+ PrintSettingsViewProxy.fallbackPaperList = fallbackPaperList;
+ PrintSettingsViewProxy.defaultSystemPrinter = defaultSystemPrinter;
+ PrintSettingsViewProxy._sourceVersion =
+ this.hasSelection && this.printSelectionOnly ? "selection" : "source";
+
+ logger.debug("availablePrinters: ", Object.keys(printersByName));
+ logger.debug("defaultSystemPrinter: ", defaultSystemPrinter);
+
+ document.addEventListener("print", async () => {
+ let cancelButton = document.getElementById("cancel-button");
+ document.l10n.setAttributes(
+ cancelButton,
+ cancelButton.dataset.closeL10nId
+ );
+ let didPrint = await this.print();
+ if (!didPrint) {
+ // Re-enable elements of the form if the user cancels saving or
+ // if a deferred task rendered the page invalid.
+ this.printForm.enable();
+ }
+ // Reset the cancel button regardless of the outcome.
+ document.l10n.setAttributes(
+ cancelButton,
+ cancelButton.dataset.cancelL10nId
+ );
+ });
+ this._createDelayedSettingsChangeTask();
+ document.addEventListener("update-print-settings", e => {
+ this.handleSettingsChange(e.detail);
+ });
+ document.addEventListener("cancel-print-settings", e => {
+ this._delayedSettingsChangeTask.disarm();
+ for (let setting of Object.keys(e.detail)) {
+ delete this._delayedChanges[setting];
+ }
+ });
+ document.addEventListener("cancel-print", () => this.cancelPrint());
+ document.addEventListener("open-system-dialog", async () => {
+ // This file in only used if pref print.always_print_silent is false, so
+ // no need to check that here.
+
+ // Hide the dialog box before opening system dialog
+ // We cannot close the window yet because the browsing context for the
+ // print preview browser is needed to print the page.
+ let sourceBrowser =
+ this.printPreviewEl.getSourceBrowsingContext().top.embedderElement;
+ let dialogBoxManager =
+ PrintUtils.getTabDialogBox(sourceBrowser).getTabDialogManager();
+ dialogBoxManager.hideDialog(sourceBrowser);
+
+ // Use our settings to prepopulate the system dialog.
+ // The system print dialog won't recognize our internal save-to-pdf
+ // pseudo-printer. We need to pass it a settings object from any
+ // system recognized printer.
+ let settings =
+ this.settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? PrintUtils.getPrintSettings(this.viewSettings.defaultSystemPrinter)
+ : this.settings.clone();
+ // We set the title so that if the user chooses save-to-PDF from the
+ // system dialog the title will be used to generate the prepopulated
+ // filename in the file picker.
+ settings.title = this.activeTitle;
+ Services.telemetry.scalarAdd("printing.dialog_opened_via_preview_tm", 1);
+ const doPrint = await this._showPrintDialog(
+ window,
+ this.hasSelection,
+ settings
+ );
+ if (!doPrint) {
+ Services.telemetry.scalarAdd(
+ "printing.dialog_via_preview_cancelled_tm",
+ 1
+ );
+ window.close();
+ return;
+ }
+ await this.print(settings);
+ });
+
+ let originalError;
+ const printersByPriority = [
+ selectedPrinter.value,
+ ...Object.getOwnPropertyNames(printersByName).filter(
+ name => name != selectedPrinter.value
+ ),
+ ];
+
+ // Try to update settings, falling back to any available printer
+ for (const printerName of printersByPriority) {
+ try {
+ let settingsToChange = await this.refreshSettings(printerName);
+ await this.updateSettings(settingsToChange, true);
+ originalError = null;
+ break;
+ } catch (e) {
+ if (!originalError) {
+ originalError = e;
+ // Report on how often fetching the last used printer settings fails.
+ this.reportPrintingError("PRINTER_SETTINGS_LAST_USED");
+ }
+ }
+ }
+
+ // Only throw original error if no fallback was possible
+ if (originalError) {
+ this.reportPrintingError("PRINTER_SETTINGS");
+ throw originalError;
+ }
+
+ let initialPreviewDone = this._updatePrintPreview();
+
+ // Use a DeferredTask for updating the preview. This will ensure that we
+ // only have one update running at a time.
+ this._createUpdatePrintPreviewTask(initialPreviewDone);
+
+ document.dispatchEvent(
+ new CustomEvent("available-destinations", {
+ detail: destinations,
+ })
+ );
+
+ document.dispatchEvent(
+ new CustomEvent("print-settings", {
+ detail: this.viewSettings,
+ })
+ );
+
+ document.body.removeAttribute("loading");
+
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ // Now that we're showing the form, select the destination select.
+ document.getElementById("printer-picker").focus({ focusVisible: true });
+
+ await initialPreviewDone;
+ },
+
+ async print(systemDialogSettings) {
+ // Disable the form when a print is in progress
+ this.printForm.disable();
+
+ if (Object.keys(this._delayedChanges).length) {
+ // Make sure any pending changes get saved.
+ let task = this._delayedSettingsChangeTask;
+ this._createDelayedSettingsChangeTask();
+ await task.finalize();
+ }
+
+ if (this.settings.pageRanges.length) {
+ // Finish any running previews to verify the range is still valid.
+ let task = this._updatePrintPreviewTask;
+ this._createUpdatePrintPreviewTask();
+ await task.finalize();
+ }
+
+ if (!this.printForm.checkValidity() || this.previewIsEmpty) {
+ return false;
+ }
+
+ let settings = systemDialogSettings || this.settings;
+
+ if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ try {
+ settings.toFileName = await pickFileName(
+ this.activeTitle,
+ this.activeURI
+ );
+ } catch (e) {
+ return false;
+ }
+ }
+
+ await window._initialized;
+
+ // This seems like it should be handled automatically but it isn't.
+ PSSVC.maybeSaveLastUsedPrinterNameToPrefs(settings.printerName);
+
+ try {
+ // We'll provide our own progress indicator.
+ let l10nId =
+ settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? "printui-print-progress-indicator-saving"
+ : "printui-print-progress-indicator";
+ document.l10n.setAttributes(this.printProgressIndicator, l10nId);
+ this.printProgressIndicator.hidden = false;
+ let bc = this.printPreviewEl.currentBrowsingContext;
+ await this._doPrint(bc, settings);
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ // Clear the file name from the preference value since it may potentially
+ // contain sensitive information from the page title (Bug 1675965)
+ let prefName =
+ "print.printer_" +
+ settings.printerName.replace(/ /g, "_") +
+ ".print_to_filename";
+ Services.prefs.clearUserPref(prefName);
+ }
+
+ window.close();
+ return true;
+ },
+
+ /**
+ * Prints the window. This method has been abstracted into a helper for
+ * testing purposes.
+ */
+ _doPrint(aBrowsingContext, aSettings) {
+ return aBrowsingContext.print(aSettings);
+ },
+
+ cancelPrint() {
+ Services.telemetry.scalarAdd("printing.preview_cancelled_tm", 1);
+ window.close();
+ },
+
+ async refreshSettings(printerName) {
+ this.currentPrinterName = printerName;
+ let currentPrinter;
+ try {
+ currentPrinter = await PrintSettingsViewProxy.resolvePropertiesForPrinter(
+ printerName
+ );
+ } catch (e) {
+ this.reportPrintingError("PRINTER_PROPERTIES");
+ throw e;
+ }
+ if (this.currentPrinterName != printerName) {
+ // Refresh settings could take a while, if the destination has changed
+ // then we don't want to update the settings after all.
+ return {};
+ }
+
+ this.settings = currentPrinter.settings;
+ this.defaultSettings = currentPrinter.defaultSettings;
+
+ this.settings.printSelectionOnly = this.printSelectionOnly;
+
+ logger.debug("currentPrinter name: ", printerName);
+ logger.debug("settings:", serializeSettings(this.settings));
+
+ // Some settings are only used by the UI
+ // assigning new values should update the underlying settings
+ this.viewSettings = new Proxy(this.settings, PrintSettingsViewProxy);
+ return this.getSettingsToUpdate();
+ },
+
+ getSettingsToUpdate() {
+ // Get the previously-changed settings we want to try to use on this printer
+ let settingsToUpdate = Object.assign({}, this._userChangedSettings);
+
+ // Ensure the color option is correct, if either of the supportsX flags are
+ // false then the user cannot change the value through the UI.
+ if (!this.viewSettings.supportsColor) {
+ settingsToUpdate.printInColor = false;
+ } else if (!this.viewSettings.supportsMonochrome) {
+ settingsToUpdate.printInColor = true;
+ }
+
+ if (settingsToUpdate.sourceVersion == "simplified") {
+ if (this.viewSettings.printBackgrounds) {
+ // Remember that this was true before so it gets restored if the
+ // format is changed to something else.
+ this._userChangedSettings.printBackgrounds = true;
+ }
+ // Backgrounds are removed in simplified mode and this setting changes
+ // the output subtly to be less legible.
+ settingsToUpdate.printBackgrounds = false;
+ }
+
+ if (
+ settingsToUpdate.printInColor != this._userChangedSettings.printInColor
+ ) {
+ delete this._userChangedSettings.printInColor;
+ }
+
+ // See if the paperId needs to change.
+ let paperId = settingsToUpdate.paperId || this.viewSettings.paperId;
+
+ logger.debug("Using paperId: ", paperId);
+ logger.debug(
+ "Available paper sizes: ",
+ PrintSettingsViewProxy.availablePaperSizes
+ );
+ let matchedPaper =
+ paperId && PrintSettingsViewProxy.availablePaperSizes[paperId];
+ if (!matchedPaper) {
+ let paperWidth, paperHeight, paperSizeUnit;
+ if (settingsToUpdate.paperId) {
+ // The user changed paperId in this instance and session,
+ // We should have details on the paper size from the previous printer
+ paperId = settingsToUpdate.paperId;
+ let cachedPaperWrapper = this.allPaperSizes[paperId];
+ // for the purposes of finding a best-size match, we'll use mm
+ paperWidth = cachedPaperWrapper.paper.width * MM_PER_POINT;
+ paperHeight = cachedPaperWrapper.paper.height * MM_PER_POINT;
+ paperSizeUnit = PrintEventHandler.settings.kPaperSizeMillimeters;
+ } else {
+ paperId = this.viewSettings.paperId;
+ logger.debug(
+ "No paperId or matchedPaper, get a new default from viewSettings:",
+ paperId
+ );
+ paperWidth = this.viewSettings.paperWidth;
+ paperHeight = this.viewSettings.paperHeight;
+ paperSizeUnit = this.viewSettings.paperSizeUnit;
+ }
+ matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
+ paperWidth,
+ paperHeight,
+ paperSizeUnit
+ );
+ }
+ if (!matchedPaper) {
+ // We didn't find a good match. Take the first paper size
+ matchedPaper = Object.values(
+ PrintSettingsViewProxy.availablePaperSizes
+ )[0];
+ delete this._userChangedSettings.paperId;
+ }
+ if (matchedPaper.id !== paperId) {
+ // The exact paper id doesn't exist for this printer
+ logger.log(
+ `Requested paperId: "${paperId}" missing on this printer, using: ${matchedPaper.id} instead`
+ );
+ delete this._userChangedSettings.paperId;
+ }
+ // Always write paper details back to settings
+ settingsToUpdate.paperId = matchedPaper.id;
+
+ return settingsToUpdate;
+ },
+
+ _createDelayedSettingsChangeTask() {
+ this._delayedSettingsChangeTask = createDeferredTask(async () => {
+ if (Object.keys(this._delayedChanges).length) {
+ let changes = this._delayedChanges;
+ this._delayedChanges = {};
+ await this.onUserSettingsChange(changes);
+ }
+ }, INPUT_DELAY_MS);
+ },
+
+ _createUpdatePrintPreviewTask(initialPreviewDone = null) {
+ this._updatePrintPreviewTask = new DeferredTask(async () => {
+ await initialPreviewDone;
+ await this._updatePrintPreview();
+ document.dispatchEvent(new CustomEvent("preview-updated"));
+ }, 0);
+ },
+
+ _scheduleDelayedSettingsChange(changes) {
+ Object.assign(this._delayedChanges, changes);
+ this._delayedSettingsChangeTask.disarm();
+ this._delayedSettingsChangeTask.arm();
+ },
+
+ handleSettingsChange(changedSettings = {}) {
+ let delayedChanges = {};
+ let instantChanges = {};
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ switch (setting) {
+ case "pageRanges":
+ case "scaling":
+ delayedChanges[setting] = value;
+ break;
+ case "customMargins":
+ delete this._delayedChanges.margins;
+ changedSettings.margins == "custom"
+ ? (delayedChanges[setting] = value)
+ : (instantChanges[setting] = value);
+ break;
+ default:
+ instantChanges[setting] = value;
+ break;
+ }
+ }
+ if (Object.keys(delayedChanges).length) {
+ this._scheduleDelayedSettingsChange(delayedChanges);
+ }
+ if (Object.keys(instantChanges).length) {
+ this.onUserSettingsChange(instantChanges);
+ }
+ },
+
+ async onUserSettingsChange(changedSettings = {}) {
+ let previewableChange = false;
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ Services.telemetry.keyedScalarAdd(
+ "printing.settings_changed",
+ setting,
+ 1
+ );
+ // Update the list of user-changed settings, which we attempt to maintain
+ // across printer changes.
+ this._userChangedSettings[setting] = value;
+ if (!this._noPreviewUpdateSettings.has(setting)) {
+ previewableChange = true;
+ }
+ }
+ if (changedSettings.printerName) {
+ logger.debug(
+ "onUserSettingsChange, changing to printerName:",
+ changedSettings.printerName
+ );
+ this.printForm.printerChanging = true;
+ this.printForm.disable(el => el.id != "printer-picker");
+ let { printerName } = changedSettings;
+ // Treat a printerName change separately, because it involves a settings
+ // object switch and we don't want to set the new name on the old settings.
+ changedSettings = await this.refreshSettings(printerName);
+ if (printerName != this.currentPrinterName) {
+ // Don't continue this update if the printer changed again.
+ return;
+ }
+ this.printForm.printerChanging = false;
+ this.printForm.enable();
+ } else {
+ changedSettings = this.getSettingsToUpdate();
+ }
+
+ let shouldPreviewUpdate =
+ (await this.updateSettings(
+ changedSettings,
+ !!changedSettings.printerName
+ )) && previewableChange;
+
+ if (shouldPreviewUpdate && !printPending) {
+ // We do not need to arm the preview task if the user has already printed
+ // and finalized any deferred tasks.
+ this.updatePrintPreview();
+ }
+ document.dispatchEvent(
+ new CustomEvent("print-settings", {
+ detail: this.viewSettings,
+ })
+ );
+ },
+
+ async updateSettings(changedSettings = {}, printerChanged = false) {
+ let updatePreviewWithoutFlag = false;
+ let flags = 0;
+ logger.debug("updateSettings ", changedSettings, printerChanged);
+
+ if (printerChanged || changedSettings.paperId) {
+ // The paper's margin properties are async,
+ // so resolve those now before we update the settings
+ try {
+ let paperWrapper = await PrintSettingsViewProxy.fetchPaperMargins(
+ changedSettings.paperId || this.viewSettings.paperId
+ );
+
+ // See if we also need to change the custom margin values
+
+ let paperHeightInInches = paperWrapper.paper.height * INCHES_PER_POINT;
+ let paperWidthInInches = paperWrapper.paper.width * INCHES_PER_POINT;
+ let height =
+ (changedSettings.orientation || this.viewSettings.orientation) == 0
+ ? paperHeightInInches
+ : paperWidthInInches;
+ let width =
+ (changedSettings.orientation || this.viewSettings.orientation) == 0
+ ? paperWidthInInches
+ : paperHeightInInches;
+
+ function verticalMarginsInvalid(margins) {
+ return (
+ parseFloat(margins.marginTop) + parseFloat(margins.marginBottom) >
+ height -
+ paperWrapper.unwriteableMarginTop -
+ paperWrapper.unwriteableMarginBottom
+ );
+ }
+
+ function horizontalMarginsInvalid(margins) {
+ return (
+ parseFloat(margins.marginRight) + parseFloat(margins.marginLeft) >
+ width -
+ paperWrapper.unwriteableMarginRight -
+ paperWrapper.unwriteableMarginLeft
+ );
+ }
+
+ let unwriteableMarginsInvalid = false;
+ if (
+ verticalMarginsInvalid(this.viewSettings.customMargins) ||
+ this.viewSettings.customMargins.marginTop < 0 ||
+ this.viewSettings.customMargins.marginBottom < 0
+ ) {
+ let { marginTop, marginBottom } = this.viewSettings.defaultMargins;
+ if (verticalMarginsInvalid(this.viewSettings.defaultMargins)) {
+ let marginsNone = this.getMarginPresets("none");
+ marginTop = marginsNone.marginTop;
+ marginBottom = marginsNone.marginBottom;
+ unwriteableMarginsInvalid = true;
+ }
+ changedSettings.marginTop = changedSettings.customMarginTop =
+ marginTop;
+ changedSettings.marginBottom = changedSettings.customMarginBottom =
+ marginBottom;
+ delete this._userChangedSettings.customMargins;
+ }
+
+ if (
+ horizontalMarginsInvalid(this.viewSettings.customMargins) ||
+ this.viewSettings.customMargins.marginLeft < 0 ||
+ this.viewSettings.customMargins.marginRight < 0
+ ) {
+ let { marginLeft, marginRight } = this.viewSettings.defaultMargins;
+ if (horizontalMarginsInvalid(this.viewSettings.defaultMargins)) {
+ let marginsNone = this.getMarginPresets("none");
+ marginLeft = marginsNone.marginLeft;
+ marginRight = marginsNone.marginRight;
+ unwriteableMarginsInvalid = true;
+ }
+ changedSettings.marginLeft = changedSettings.customMarginLeft =
+ marginLeft;
+ changedSettings.marginRight = changedSettings.customMarginRight =
+ marginRight;
+ delete this._userChangedSettings.customMargins;
+ }
+
+ if (unwriteableMarginsInvalid) {
+ changedSettings.ignoreUnwriteableMargins = true;
+ }
+ } catch (e) {
+ this.reportPrintingError("PAPER_MARGINS");
+ throw e;
+ }
+ }
+
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ // Always write paper changes back to settings as pref-derived values could be bad
+ if (
+ this.viewSettings[setting] != value ||
+ (printerChanged && setting == "paperId")
+ ) {
+ if (setting == "pageRanges") {
+ // The page range is kept as an array. If the user switches between all
+ // and custom with no specified range input (which is represented as an
+ // empty array), we do not want to send an update.
+ if (!this.viewSettings[setting].length && !value.length) {
+ continue;
+ }
+ }
+ this.viewSettings[setting] = value;
+
+ if (
+ setting in this.settingFlags &&
+ setting in this._userChangedSettings
+ ) {
+ flags |= this.settingFlags[setting];
+ }
+ updatePreviewWithoutFlag |=
+ this._nonFlaggedUpdatePreviewSettings.has(setting);
+ }
+ }
+
+ let shouldPreviewUpdate =
+ flags || printerChanged || updatePreviewWithoutFlag;
+ logger.debug(
+ "updateSettings, calculated flags:",
+ flags,
+ "shouldPreviewUpdate:",
+ shouldPreviewUpdate
+ );
+ if (flags) {
+ this.saveSettingsToPrefs(flags);
+ }
+ return shouldPreviewUpdate;
+ },
+
+ saveSettingsToPrefs(flags) {
+ PSSVC.maybeSavePrintSettingsToPrefs(this.settings, flags);
+ },
+
+ /**
+ * Queue a task to update the print preview. It will start immediately or when
+ * the in progress update completes.
+ */
+ async updatePrintPreview() {
+ // Make sure the rendering state is set so we don't visibly update the
+ // sheet count with incomplete data.
+ this._updatePrintPreviewTask.arm();
+ },
+
+ /**
+ * Creates a print preview or refreshes the preview with new settings when omitted.
+ *
+ * @return {Promise} Resolves when the preview has been updated.
+ */
+ async _updatePrintPreview() {
+ let { settings } = this;
+
+ const isFirstCall = !this.printInitiationTime;
+ if (isFirstCall) {
+ let params = new URLSearchParams(location.search);
+ this.printInitiationTime = parseInt(
+ params.get("printInitiationTime"),
+ 10
+ );
+ const elapsed = Date.now() - this.printInitiationTime;
+ Services.telemetry
+ .getHistogramById("PRINT_INIT_TO_PLATFORM_SENT_SETTINGS_MS")
+ .add(elapsed);
+ }
+
+ let totalPageCount, sheetCount, isEmpty, orientation, pageWidth, pageHeight;
+ try {
+ // This resolves with a PrintPreviewSuccessInfo dictionary.
+ let { sourceVersion } = this.viewSettings;
+ let sourceURI = this.activeURI;
+ this._lastPrintPreviewSettings = settings;
+ ({
+ totalPageCount,
+ sheetCount,
+ isEmpty,
+ orientation,
+ pageWidth,
+ pageHeight,
+ } = await this.printPreviewEl.printPreview(settings, {
+ sourceVersion,
+ sourceURI,
+ }));
+ } catch (e) {
+ this.reportPrintingError("PRINT_PREVIEW");
+ console.error(e);
+ throw e;
+ }
+
+ // If there is a set orientation, update the settings to use it. In this
+ // case, the document will already have used this orientation to create
+ // the print preview.
+ if (orientation != "unspecified") {
+ const kIPrintSettings = Ci.nsIPrintSettings;
+ settings.orientation =
+ orientation == "landscape"
+ ? kIPrintSettings.kLandscapeOrientation
+ : kIPrintSettings.kPortraitOrientation;
+ document.dispatchEvent(new CustomEvent("hide-orientation"));
+ }
+
+ // If the page size is set, check whether we should use it as our paper size.
+ let isUsingPageRuleSizeAsPaperSize =
+ settings.usePageRuleSizeAsPaperSize &&
+ pageWidth !== null &&
+ pageHeight !== null;
+ if (isUsingPageRuleSizeAsPaperSize) {
+ // We canonically represent paper sizes using the width/height of a portrait-oriented sheet,
+ // with landscape-orientation applied as a supplemental rotation.
+ // If the page-size is landscape oriented, we flip the pageWidth / pageHeight here
+ // in order to pass a canonical representation into the paper-size settings.
+ if (orientation == "landscape") {
+ [pageHeight, pageWidth] = [pageWidth, pageHeight];
+ }
+
+ let matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
+ pageWidth,
+ pageHeight,
+ settings.kPaperSizeInches
+ );
+ if (matchedPaper) {
+ settings.paperId = matchedPaper.id;
+ }
+
+ settings.paperWidth = pageWidth;
+ settings.paperHeight = pageHeight;
+ settings.paperSizeUnit = settings.kPaperSizeInches;
+ document.dispatchEvent(new CustomEvent("hide-paper-size"));
+ }
+
+ this.previewIsEmpty = isEmpty;
+ // If the preview is empty, we know our range is greater than the number of pages.
+ // We have to send a pageRange update to display a non-empty page.
+ if (this.previewIsEmpty) {
+ this.viewSettings.pageRanges = [];
+ this.updatePrintPreview();
+ }
+
+ document.dispatchEvent(
+ new CustomEvent("page-count", {
+ detail: { sheetCount, totalPages: totalPageCount },
+ })
+ );
+
+ if (isFirstCall) {
+ const elapsed = Date.now() - this.printInitiationTime;
+ Services.telemetry
+ .getHistogramById("PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS")
+ .add(elapsed);
+ }
+ },
+
+ async getPrintDestinations() {
+ const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
+ Ci.nsIPrinterList
+ );
+ let printers;
+
+ if (Cu.isInAutomation) {
+ printers = window._mockPrinters || [];
+ } else {
+ try {
+ printers = await printerList.printers;
+ } catch (e) {
+ this.reportPrintingError("PRINTER_LIST");
+ throw e;
+ }
+ }
+
+ let fallbackPaperList;
+ try {
+ fallbackPaperList = await printerList.fallbackPaperList;
+ } catch (e) {
+ this.reportPrintingError("FALLBACK_PAPER_LIST");
+ throw e;
+ }
+
+ let lastUsedPrinterName;
+ try {
+ lastUsedPrinterName = PSSVC.lastUsedPrinterName;
+ } catch (e) {
+ this.reportPrintingError("LAST_USED_PRINTER");
+ throw e;
+ }
+ const defaultPrinterName = printerList.systemDefaultPrinterName;
+ const printersByName = {};
+
+ let lastUsedPrinter;
+ let defaultSystemPrinter;
+
+ let saveToPdfPrinter = {
+ nameId: "printui-destination-pdf-label",
+ value: PrintUtils.SAVE_TO_PDF_PRINTER,
+ };
+ printersByName[PrintUtils.SAVE_TO_PDF_PRINTER] = {
+ supportsColor: true,
+ supportsMonochrome: false,
+ name: PrintUtils.SAVE_TO_PDF_PRINTER,
+ };
+
+ if (lastUsedPrinterName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ lastUsedPrinter = saveToPdfPrinter;
+ }
+
+ let destinations = [
+ saveToPdfPrinter,
+ ...printers.map(printer => {
+ printer.QueryInterface(Ci.nsIPrinter);
+ const { name } = printer;
+ printersByName[printer.name] = { printer };
+ const destination = { name, value: name };
+
+ if (name == lastUsedPrinterName) {
+ lastUsedPrinter = destination;
+ }
+ if (name == defaultPrinterName) {
+ defaultSystemPrinter = destination;
+ }
+
+ return destination;
+ }),
+ ];
+
+ let selectedPrinter =
+ lastUsedPrinter || defaultSystemPrinter || saveToPdfPrinter;
+
+ return {
+ destinations,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName,
+ defaultSystemPrinter,
+ };
+ },
+
+ getMarginPresets(marginSize, paperWrapper) {
+ switch (marginSize) {
+ case "minimum": {
+ let marginSource = paperWrapper || this.defaultSettings;
+ return {
+ marginTop: marginSource.unwriteableMarginTop,
+ marginRight: marginSource.unwriteableMarginRight,
+ marginBottom: marginSource.unwriteableMarginBottom,
+ marginLeft: marginSource.unwriteableMarginLeft,
+ };
+ }
+ case "none":
+ return {
+ marginTop: 0,
+ marginLeft: 0,
+ marginBottom: 0,
+ marginRight: 0,
+ };
+ case "custom":
+ return {
+ marginTop:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginTop ??
+ this.settings.marginTop,
+ marginBottom:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginBottom ??
+ this.settings.marginBottom,
+ marginLeft:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginLeft ??
+ this.settings.marginLeft,
+ marginRight:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginRight ??
+ this.settings.marginRight,
+ };
+ default: {
+ let minimum = this.getMarginPresets("minimum", paperWrapper);
+ return {
+ marginTop: !isNaN(minimum.marginTop)
+ ? Math.max(minimum.marginTop, this.defaultSettings.marginTop)
+ : this.defaultSettings.marginTop,
+ marginRight: !isNaN(minimum.marginRight)
+ ? Math.max(minimum.marginRight, this.defaultSettings.marginRight)
+ : this.defaultSettings.marginRight,
+ marginBottom: !isNaN(minimum.marginBottom)
+ ? Math.max(minimum.marginBottom, this.defaultSettings.marginBottom)
+ : this.defaultSettings.marginBottom,
+ marginLeft: !isNaN(minimum.marginLeft)
+ ? Math.max(minimum.marginLeft, this.defaultSettings.marginLeft)
+ : this.defaultSettings.marginLeft,
+ };
+ }
+ }
+ },
+
+ reportPrintingError(aMessage) {
+ logger.debug("reportPrintingError:", aMessage);
+ Services.telemetry.keyedScalarAdd("printing.error", aMessage, 1);
+ },
+
+ /**
+ * Shows the system dialog. This method has been abstracted into a helper for
+ * testing purposes. The showPrintDialog() call blocks until the dialog is
+ * closed, so we mark it as async to allow us to reject from the test.
+ */
+ async _showPrintDialog(aWindow, aHaveSelection, aSettings) {
+ return PrintUtils.handleSystemPrintDialog(
+ aWindow,
+ aHaveSelection,
+ aSettings
+ );
+ },
+};
+
+var PrintSettingsViewProxy = {
+ get defaultHeadersAndFooterValues() {
+ const defaultBranch = Services.prefs.getDefaultBranch("");
+ let settingValues = {};
+ for (let [name, pref] of Object.entries(this.headerFooterSettingsPrefs)) {
+ settingValues[name] = defaultBranch.getStringPref(pref);
+ }
+ // We only need to retrieve these defaults once and they will not change
+ Object.defineProperty(this, "defaultHeadersAndFooterValues", {
+ value: settingValues,
+ });
+ return settingValues;
+ },
+
+ headerFooterSettingsPrefs: {
+ footerStrCenter: "print.print_footercenter",
+ footerStrLeft: "print.print_footerleft",
+ footerStrRight: "print.print_footerright",
+ headerStrCenter: "print.print_headercenter",
+ headerStrLeft: "print.print_headerleft",
+ headerStrRight: "print.print_headerright",
+ },
+
+ // Custom margins are not saved by a pref, so we need to keep track of them
+ // in order to save the value.
+ _lastCustomMarginValues: {
+ marginTop: null,
+ marginBottom: null,
+ marginLeft: null,
+ marginRight: null,
+ },
+
+ // This list was taken from nsDeviceContextSpecWin.cpp which records telemetry on print target type
+ knownSaveToFilePrinters: new Set([
+ "Microsoft Print to PDF",
+ "Adobe PDF",
+ "Bullzip PDF Printer",
+ "CutePDF Writer",
+ "doPDF",
+ "Foxit Reader PDF Printer",
+ "Nitro PDF Creator",
+ "novaPDF",
+ "PDF-XChange",
+ "PDF24 PDF",
+ "PDFCreator",
+ "PrimoPDF",
+ "Soda PDF",
+ "Solid PDF Creator",
+ "Universal Document Converter",
+ "Microsoft XPS Document Writer",
+ ]),
+
+ getBestPaperMatch(paperWidth, paperHeight, paperSizeUnit) {
+ let paperSizes = Object.values(this.availablePaperSizes);
+ if (!(paperWidth && paperHeight)) {
+ return null;
+ }
+ // first try to match on the paper dimensions using the current units
+ let unitsPerPoint;
+ let altUnitsPerPoint;
+ if (paperSizeUnit == PrintEventHandler.settings.kPaperSizeMillimeters) {
+ unitsPerPoint = MM_PER_POINT;
+ altUnitsPerPoint = INCHES_PER_POINT;
+ } else {
+ unitsPerPoint = INCHES_PER_POINT;
+ altUnitsPerPoint = MM_PER_POINT;
+ }
+ // equality to 1pt.
+ const equal = (a, b) => Math.abs(a - b) < 1;
+ const findMatch = (widthPts, heightPts) =>
+ paperSizes.find(paperWrapper => {
+ // the dimensions on the nsIPaper object are in points
+ let result =
+ equal(widthPts, paperWrapper.paper.width) &&
+ equal(heightPts, paperWrapper.paper.height);
+ return result;
+ });
+ // Look for a paper with matching dimensions, using the current printer's
+ // paper size unit, then the alternate unit
+ let matchedPaper =
+ findMatch(paperWidth / unitsPerPoint, paperHeight / unitsPerPoint) ||
+ findMatch(paperWidth / altUnitsPerPoint, paperHeight / altUnitsPerPoint);
+
+ if (matchedPaper) {
+ return matchedPaper;
+ }
+ return null;
+ },
+
+ async fetchPaperMargins(paperId) {
+ // resolve any async and computed properties we need on the paper
+ let paperWrapper = this.availablePaperSizes[paperId];
+ if (!paperWrapper) {
+ throw new Error("Can't fetchPaperMargins: " + paperId);
+ }
+ if (paperWrapper._resolved) {
+ // We've already resolved and calculated these values
+ return paperWrapper;
+ }
+ let margins;
+ try {
+ margins = await paperWrapper.paper.unwriteableMargin;
+ } catch (e) {
+ this.reportPrintingError("UNWRITEABLE_MARGIN");
+ throw e;
+ }
+ margins.QueryInterface(Ci.nsIPaperMargin);
+
+ // margin dimensions are given on the paper in points, setting values need to be in inches
+ paperWrapper.unwriteableMarginTop = margins.top * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginRight = margins.right * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginBottom = margins.bottom * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginLeft = margins.left * INCHES_PER_POINT;
+ // No need to re-resolve static properties
+ paperWrapper._resolved = true;
+ return paperWrapper;
+ },
+
+ async resolvePropertiesForPrinter(printerName) {
+ // resolve any async properties we need on the printer
+ let printerInfo = this.availablePrinters[printerName];
+ if (printerInfo._resolved) {
+ // Store a convenience reference
+ this.availablePaperSizes = printerInfo.availablePaperSizes;
+ return printerInfo;
+ }
+
+ // Await the async printer data.
+ if (printerInfo.printer) {
+ let basePrinterInfo;
+ try {
+ [
+ printerInfo.supportsDuplex,
+ printerInfo.supportsColor,
+ printerInfo.supportsMonochrome,
+ basePrinterInfo,
+ ] = await Promise.all([
+ printerInfo.printer.supportsDuplex,
+ printerInfo.printer.supportsColor,
+ printerInfo.printer.supportsMonochrome,
+ printerInfo.printer.printerInfo,
+ ]);
+ } catch (e) {
+ this.reportPrintingError("PRINTER_SETTINGS");
+ throw e;
+ }
+ basePrinterInfo.QueryInterface(Ci.nsIPrinterInfo);
+ basePrinterInfo.defaultSettings.QueryInterface(Ci.nsIPrintSettings);
+
+ printerInfo.paperList = basePrinterInfo.paperList;
+ printerInfo.defaultSettings = basePrinterInfo.defaultSettings;
+ } else if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ // The Mozilla PDF pseudo-printer has no actual nsIPrinter implementation
+ printerInfo.defaultSettings = PSSVC.createNewPrintSettings();
+ printerInfo.defaultSettings.printerName = printerName;
+ printerInfo.defaultSettings.toFileName = "";
+ printerInfo.defaultSettings.outputFormat =
+ Ci.nsIPrintSettings.kOutputFormatPDF;
+ printerInfo.defaultSettings.outputDestination =
+ Ci.nsIPrintSettings.kOutputDestinationFile;
+ printerInfo.defaultSettings.usePageRuleSizeAsPaperSize =
+ Services.prefs.getBoolPref(
+ "print.save_as_pdf.use_page_rule_size_as_paper_size.enabled",
+ false
+ );
+ printerInfo.paperList = this.fallbackPaperList;
+ }
+ printerInfo.settings = printerInfo.defaultSettings.clone();
+ // Apply any previously persisted user values
+ // Don't apply kInitSavePrintToFile though, that should only be true for
+ // the PDF printer.
+ printerInfo.settings.outputDestination =
+ printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? Ci.nsIPrintSettings.kOutputDestinationFile
+ : Ci.nsIPrintSettings.kOutputDestinationPrinter;
+ let flags =
+ printerInfo.settings.kInitSaveAll ^
+ printerInfo.settings.kInitSavePrintToFile;
+ PSSVC.initPrintSettingsFromPrefs(printerInfo.settings, true, flags);
+ // We set `isInitializedFromPrinter` to make sure that that's set on the
+ // SAVE_TO_PDF_PRINTER settings. The naming is poor, but that tells the
+ // platform code that the settings object is complete.
+ printerInfo.settings.isInitializedFromPrinter = true;
+
+ printerInfo.settings.toFileName = "";
+
+ // prepare the available paper sizes for this printer
+ if (!printerInfo.paperList?.length) {
+ logger.warn(
+ "Printer has empty paperList: ",
+ printerInfo.printer.id,
+ "using fallbackPaperList"
+ );
+ printerInfo.paperList = this.fallbackPaperList;
+ }
+ // don't trust the settings to provide valid paperSizeUnit values
+ let sizeUnit =
+ printerInfo.settings.paperSizeUnit ==
+ printerInfo.settings.kPaperSizeMillimeters
+ ? printerInfo.settings.kPaperSizeMillimeters
+ : printerInfo.settings.kPaperSizeInches;
+ let papersById = (printerInfo.availablePaperSizes = {});
+ // Store a convenience reference
+ this.availablePaperSizes = papersById;
+
+ for (let paper of printerInfo.paperList) {
+ paper.QueryInterface(Ci.nsIPaper);
+ // Bug 1662239: I'm seeing multiple duplicate entries for each paper size
+ // so ensure we have one entry per name
+ if (!papersById[paper.id]) {
+ papersById[paper.id] = {
+ paper,
+ id: paper.id,
+ name: paper.name,
+ // XXXsfoster: Eventually we want to get the unit from the nsIPaper object
+ sizeUnit,
+ };
+ }
+ }
+ // Update our cache of all the paper sizes by name
+ Object.assign(PrintEventHandler.allPaperSizes, papersById);
+
+ // The printer properties don't change, mark this as resolved for next time
+ printerInfo._resolved = true;
+ logger.debug("Resolved printerInfo:", printerInfo);
+ return printerInfo;
+ },
+
+ get(target, name) {
+ switch (name) {
+ case "currentPaper": {
+ let paperId = this.get(target, "paperId");
+ return paperId && this.availablePaperSizes[paperId];
+ }
+
+ case "marginPresets":
+ let paperWrapper = this.get(target, "currentPaper");
+ return {
+ none: PrintEventHandler.getMarginPresets("none", paperWrapper),
+ minimum: PrintEventHandler.getMarginPresets("minimum", paperWrapper),
+ default: PrintEventHandler.getMarginPresets("default", paperWrapper),
+ custom: PrintEventHandler.getMarginPresets("custom", paperWrapper),
+ };
+
+ case "marginOptions": {
+ let allMarginPresets = this.get(target, "marginPresets");
+ let uniqueMargins = new Set();
+ let marginsEnabled = {};
+ for (let name of ["none", "default", "minimum", "custom"]) {
+ let { marginTop, marginLeft, marginBottom, marginRight } =
+ allMarginPresets[name];
+ let key = [marginTop, marginLeft, marginBottom, marginRight].join(
+ ","
+ );
+ // Custom margins are initialized to default margins
+ marginsEnabled[name] = !uniqueMargins.has(key) || name == "custom";
+ uniqueMargins.add(key);
+ }
+ return marginsEnabled;
+ }
+
+ case "margins":
+ let marginSettings = {
+ marginTop: target.marginTop,
+ marginLeft: target.marginLeft,
+ marginBottom: target.marginBottom,
+ marginRight: target.marginRight,
+ };
+ // see if they match the none, minimum, or default margin values
+ let allMarginPresets = this.get(target, "marginPresets");
+ const marginsMatch = function (lhs, rhs) {
+ return Object.keys(marginSettings).every(
+ name => lhs[name].toFixed(2) == rhs[name].toFixed(2)
+ );
+ };
+ const potentialPresets = (function () {
+ let presets = [];
+ const minimumIsNone = marginsMatch(
+ allMarginPresets.none,
+ allMarginPresets.minimum
+ );
+ // We only attempt to match the serialized values against the "none"
+ // preset if the unwriteable margins are being ignored or are zero.
+ if (target.ignoreUnwriteableMargins || minimumIsNone) {
+ presets.push("none");
+ }
+ if (!minimumIsNone) {
+ presets.push("minimum");
+ }
+ presets.push("default");
+ return presets;
+ })();
+ for (let presetName of potentialPresets) {
+ let marginPresets = allMarginPresets[presetName];
+ if (marginsMatch(marginSettings, marginPresets)) {
+ return presetName;
+ }
+ }
+
+ // Fall back to custom for other values
+ return "custom";
+
+ case "defaultMargins":
+ return PrintEventHandler.getMarginPresets(
+ "default",
+ this.get(target, "currentPaper")
+ );
+
+ case "customMargins":
+ return PrintEventHandler.getMarginPresets(
+ "custom",
+ this.get(target, "currentPaper")
+ );
+
+ case "paperSizes":
+ return Object.values(this.availablePaperSizes)
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map(paper => {
+ return {
+ name: paper.name,
+ value: paper.id,
+ };
+ });
+
+ case "supportsDuplex":
+ return this.availablePrinters[target.printerName].supportsDuplex;
+
+ case "printDuplex":
+ switch (target.duplex) {
+ case Ci.nsIPrintSettings.kDuplexNone:
+ break;
+ case Ci.nsIPrintSettings.kDuplexFlipOnLongEdge:
+ return "long-edge";
+ case Ci.nsIPrintSettings.kDuplexFlipOnShortEdge:
+ return "short-edge";
+ default:
+ logger.warn("Unexpected duplex value: ", target.duplex);
+ }
+ return "off";
+ case "printBackgrounds":
+ return target.printBGImages || target.printBGColors;
+
+ case "printFootersHeaders":
+ // if any of the footer and headers settings have a non-empty string value
+ // we consider that "enabled"
+ return Object.keys(this.headerFooterSettingsPrefs).some(
+ name => !!target[name]
+ );
+
+ case "supportsColor":
+ return this.availablePrinters[target.printerName].supportsColor;
+
+ case "willSaveToFile":
+ return (
+ target.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF ||
+ this.knownSaveToFilePrinters.has(target.printerName)
+ );
+ case "supportsMonochrome":
+ return this.availablePrinters[target.printerName].supportsMonochrome;
+ case "defaultSystemPrinter":
+ return (
+ this.defaultSystemPrinter?.value ||
+ Object.getOwnPropertyNames(this.availablePrinters).find(
+ name => name != PrintUtils.SAVE_TO_PDF_PRINTER
+ )
+ );
+
+ case "numCopies":
+ return this.get(target, "willSaveToFile") ? 1 : target.numCopies;
+ case "sourceVersion":
+ return this._sourceVersion;
+ }
+ return target[name];
+ },
+
+ set(target, name, value) {
+ switch (name) {
+ case "margins":
+ if (!["default", "minimum", "none", "custom"].includes(value)) {
+ logger.warn("Unexpected margin preset name: ", value);
+ value = "default";
+ }
+ let paperWrapper = this.get(target, "currentPaper");
+ let marginPresets = PrintEventHandler.getMarginPresets(
+ value,
+ paperWrapper
+ );
+ for (let [settingName, presetValue] of Object.entries(marginPresets)) {
+ target[settingName] = presetValue;
+ }
+ target.honorPageRuleMargins = value == "default";
+ target.ignoreUnwriteableMargins = value == "none";
+ break;
+
+ case "paperId": {
+ let paperId = value;
+ let paperWrapper = this.availablePaperSizes[paperId];
+ // Dimensions on the paper object are in pts.
+ // We convert to the printer's specified unit when updating settings
+ let unitsPerPoint =
+ paperWrapper.sizeUnit == target.kPaperSizeMillimeters
+ ? MM_PER_POINT
+ : INCHES_PER_POINT;
+ // paperWidth and paperHeight are calculated values that we always treat as suspect and
+ // re-calculate whenever the paperId changes
+ target.paperSizeUnit = paperWrapper.sizeUnit;
+ target.paperWidth = paperWrapper.paper.width * unitsPerPoint;
+ target.paperHeight = paperWrapper.paper.height * unitsPerPoint;
+ // Unwriteable margins were pre-calculated from their async values when the paper size
+ // was selected. They are always in inches
+ target.unwriteableMarginTop = paperWrapper.unwriteableMarginTop;
+ target.unwriteableMarginRight = paperWrapper.unwriteableMarginRight;
+ target.unwriteableMarginBottom = paperWrapper.unwriteableMarginBottom;
+ target.unwriteableMarginLeft = paperWrapper.unwriteableMarginLeft;
+ target.paperId = paperWrapper.paper.id;
+ // pull new margin values for the new paper size
+ this.set(target, "margins", this.get(target, "margins"));
+ break;
+ }
+
+ case "printerName":
+ // Can't set printerName, settings objects belong to a specific printer.
+ break;
+
+ case "printBackgrounds":
+ target.printBGImages = value;
+ target.printBGColors = value;
+ break;
+
+ case "printDuplex": {
+ let duplex = (function () {
+ switch (value) {
+ case "off":
+ break;
+ case "long-edge":
+ return Ci.nsIPrintSettings.kDuplexFlipOnLongEdge;
+ case "short-edge":
+ return Ci.nsIPrintSettings.kDuplexFlipOnShortEdge;
+ default:
+ logger.warn("Unexpected duplex name: ", value);
+ }
+ return Ci.nsIPrintSettings.kDuplexNone;
+ })();
+
+ target.duplex = duplex;
+ break;
+ }
+
+ case "printFootersHeaders":
+ // To disable header & footers, set them all to empty.
+ // To enable, restore default values for each of the header & footer settings.
+ for (let [settingName, defaultValue] of Object.entries(
+ this.defaultHeadersAndFooterValues
+ )) {
+ target[settingName] = value ? defaultValue : "";
+ }
+ break;
+
+ case "customMargins":
+ if (value != null) {
+ for (let [settingName, newVal] of Object.entries(value)) {
+ target[settingName] = newVal;
+ this._lastCustomMarginValues[settingName] = newVal;
+ }
+ }
+ break;
+
+ case "customMarginTop":
+ case "customMarginBottom":
+ case "customMarginLeft":
+ case "customMarginRight":
+ let customMarginName = "margin" + name.substring(12);
+ this.set(
+ target,
+ "customMargins",
+ Object.assign({}, this.get(target, "customMargins"), {
+ [customMarginName]: value,
+ })
+ );
+ break;
+
+ case "sourceVersion":
+ this._sourceVersion = value;
+ this.set(target, "printSelectionOnly", value == "selection");
+ if (value == "simplified") {
+ this.set(target, "printBackgrounds", false);
+ }
+ break;
+
+ default:
+ target[name] = value;
+ }
+ },
+};
+
+/*
+ * Custom elements ----------------------------------------------------
+ */
+
+function PrintUIControlMixin(superClass) {
+ return class PrintUIControl extends superClass {
+ connectedCallback() {
+ this.setAttribute("autocomplete", "off");
+ this.initialize();
+ this.render();
+ }
+
+ initialize() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ if (this.templateId) {
+ let template = this.ownerDocument.getElementById(this.templateId);
+ let templateContent = template.content;
+ this.appendChild(templateContent.cloneNode(true));
+ }
+
+ document.addEventListener("print-settings", ({ detail: settings }) => {
+ this.update(settings);
+ });
+
+ this.addEventListener("input", this);
+ }
+
+ render() {}
+
+ update(settings) {}
+
+ dispatchSettingsChange(changedSettings) {
+ this.dispatchEvent(
+ new CustomEvent("update-print-settings", {
+ bubbles: true,
+ detail: changedSettings,
+ })
+ );
+ }
+
+ cancelSettingsChange(changedSettings) {
+ this.dispatchEvent(
+ new CustomEvent("cancel-print-settings", {
+ bubbles: true,
+ detail: changedSettings,
+ })
+ );
+ }
+
+ handleEvent(event) {}
+ };
+}
+
+class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) {
+ initialize() {
+ super.initialize();
+
+ this.addEventListener("submit", this);
+ this.addEventListener("click", this);
+ this.addEventListener("revalidate", this);
+
+ this._printerDestination = this.querySelector("#destination");
+ this.printButton = this.querySelector("#print-button");
+ }
+
+ removeNonPdfSettings() {
+ let selectors = ["#backgrounds", "#source-version-selection"];
+ for (let selector of selectors) {
+ this.querySelector(selector).remove();
+ }
+ let moreSettings = this.querySelector("#more-settings-options");
+ if (moreSettings.children.length <= 1) {
+ moreSettings.remove();
+ }
+ }
+
+ requestPrint() {
+ this.requestSubmit(this.printButton);
+ }
+
+ update(settings) {
+ // If there are no default system printers available and we are not on mac,
+ // we should hide the system dialog because it won't be populated with
+ // the correct settings. Mac and Gtk support save to pdf functionality
+ // in the native dialog, so it can be shown regardless.
+ this.querySelector("#system-print").hidden =
+ AppConstants.platform === "win" && !settings.defaultSystemPrinter;
+
+ this.querySelector("#two-sided-printing").hidden = !settings.supportsDuplex;
+ }
+
+ enable() {
+ let isValid = this.checkValidity();
+ document.body.toggleAttribute("invalid", !isValid);
+ if (isValid) {
+ for (let element of this.elements) {
+ if (!element.hasAttribute("disallowed")) {
+ element.disabled = false;
+ }
+ }
+ // aria-describedby will usually cause the first value to be reported.
+ // Unfortunately, screen readers don't pick up description changes from
+ // dialogs, so we must use a live region. To avoid double reporting of
+ // the first value, we don't set aria-live initially. We only set it for
+ // subsequent updates.
+ // aria-live is set on the parent because sheetCount itself might be
+ // hidden and then shown, and updates are only reported for live
+ // regions that were already visible.
+ document
+ .querySelector("#sheet-count")
+ .parentNode.setAttribute("aria-live", "polite");
+ } else {
+ // Find the invalid element
+ let invalidElement;
+ for (let element of this.elements) {
+ if (!element.checkValidity()) {
+ invalidElement = element;
+ break;
+ }
+ }
+ let section = invalidElement.closest(".section-block");
+ document.body.toggleAttribute("invalid", !isValid);
+ // We're hiding the sheet count and aria-describedby includes the
+ // content of hidden elements, so remove aria-describedby.
+ document.body.removeAttribute("aria-describedby");
+ for (let element of this.elements) {
+ // If we're valid, enable all inputs.
+ // Otherwise, disable the valid inputs other than the cancel button and the elements
+ // in the invalid section.
+ element.disabled =
+ element.hasAttribute("disallowed") ||
+ (!isValid &&
+ element.validity.valid &&
+ element.name != "cancel" &&
+ element.closest(".section-block") != this._printerDestination &&
+ element.closest(".section-block") != section);
+ }
+ }
+ }
+
+ disable(filterFn) {
+ for (let element of this.elements) {
+ if (filterFn && !filterFn(element)) {
+ continue;
+ }
+ element.disabled = element.name != "cancel";
+ }
+ }
+
+ handleEvent(e) {
+ if (e.target.id == "open-dialog-link") {
+ this.dispatchEvent(new Event("open-system-dialog", { bubbles: true }));
+ return;
+ }
+
+ if (e.type == "submit") {
+ e.preventDefault();
+ if (e.submitter.name == "print" && this.checkValidity()) {
+ this.dispatchEvent(new Event("print", { bubbles: true }));
+ }
+ } else if (
+ (e.type == "input" || e.type == "revalidate") &&
+ !this.printerChanging
+ ) {
+ this.enable();
+ }
+ }
+}
+customElements.define("print-form", PrintUIForm, { extends: "form" });
+
+class PrintSettingSelect extends PrintUIControlMixin(HTMLSelectElement) {
+ initialize() {
+ super.initialize();
+ this.addEventListener("keypress", this);
+ }
+
+ connectedCallback() {
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ setOptions(optionValues = []) {
+ this.textContent = "";
+ for (let optionData of optionValues) {
+ let opt = new Option(
+ optionData.name,
+ "value" in optionData ? optionData.value : optionData.name
+ );
+ if (optionData.nameId) {
+ document.l10n.setAttributes(opt, optionData.nameId);
+ }
+ // option selectedness is set via update() and assignment to this.value
+ this.options.add(opt);
+ }
+ }
+
+ update(settings) {
+ if (this.settingName) {
+ this.value = settings[this.settingName];
+ }
+ }
+
+ handleEvent(e) {
+ if (e.type == "input" && this.settingName) {
+ this.dispatchSettingsChange({
+ [this.settingName]: e.target.value,
+ });
+ } else if (e.type == "keypress") {
+ if (
+ e.key == "Enter" &&
+ (!e.metaKey || AppConstants.platform == "macosx")
+ ) {
+ this.form.requestPrint();
+ }
+ }
+ }
+}
+customElements.define("setting-select", PrintSettingSelect, {
+ extends: "select",
+});
+
+class PrintSettingNumber extends PrintUIControlMixin(HTMLInputElement) {
+ initialize() {
+ super.initialize();
+ this.addEventListener("beforeinput", e => this.preventWhitespaceEntry(e));
+ this.addEventListener("paste", e => this.pasteWithoutWhitespace(e));
+ }
+
+ connectedCallback() {
+ this.type = "number";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ if (this.settingName) {
+ this.value = settings[this.settingName];
+ }
+ }
+
+ preventWhitespaceEntry(e) {
+ if (e.data && !e.data.trim().length) {
+ e.preventDefault();
+ }
+ }
+
+ pasteWithoutWhitespace(e) {
+ // Prevent original value from being pasted
+ e.preventDefault();
+
+ // Manually update input's value with sanitized clipboard data
+ let paste = (e.clipboardData || window.clipboardData)
+ .getData("text")
+ .trim();
+ this.value = paste;
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "input":
+ if (this.settingName && this.checkValidity()) {
+ this.dispatchSettingsChange({
+ [this.settingName]: this.value,
+ });
+ }
+ break;
+ }
+ }
+}
+customElements.define("setting-number", PrintSettingNumber, {
+ extends: "input",
+});
+
+class PrintSettingCheckbox extends PrintUIControlMixin(HTMLInputElement) {
+ connectedCallback() {
+ this.type = "checkbox";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ this.checked = settings[this.settingName];
+ }
+
+ handleEvent(e) {
+ this.dispatchSettingsChange({
+ [this.settingName]: this.checked,
+ });
+ }
+}
+customElements.define("setting-checkbox", PrintSettingCheckbox, {
+ extends: "input",
+});
+
+class PrintSettingRadio extends PrintUIControlMixin(HTMLInputElement) {
+ connectedCallback() {
+ this.type = "radio";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ this.checked = settings[this.settingName] == this.value;
+ }
+
+ handleEvent(e) {
+ this.dispatchSettingsChange({
+ [this.settingName]: this.value,
+ });
+ }
+}
+customElements.define("setting-radio", PrintSettingRadio, {
+ extends: "input",
+});
+
+class DestinationPicker extends PrintSettingSelect {
+ initialize() {
+ super.initialize();
+ document.addEventListener("available-destinations", this);
+ }
+
+ update(settings) {
+ super.update(settings);
+ let isPdf = settings.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF;
+ this.setAttribute("output", isPdf ? "pdf" : "paper");
+ }
+
+ handleEvent(e) {
+ super.handleEvent(e);
+
+ if (e.type == "available-destinations") {
+ this.setOptions(e.detail);
+ }
+ }
+}
+customElements.define("destination-picker", DestinationPicker, {
+ extends: "select",
+});
+
+class ColorModePicker extends PrintSettingSelect {
+ update(settings) {
+ this.value = settings[this.settingName] ? "color" : "bw";
+ let canSwitch = settings.supportsColor && settings.supportsMonochrome;
+ if (this.disablePicker != canSwitch) {
+ this.toggleAttribute("disallowed", !canSwitch);
+ this.disabled = !canSwitch;
+ }
+ this.disablePicker = canSwitch;
+ }
+
+ handleEvent(e) {
+ if (e.type == "input") {
+ // turn our string value into the expected boolean
+ this.dispatchSettingsChange({
+ [this.settingName]: this.value == "color",
+ });
+ }
+ }
+}
+customElements.define("color-mode-select", ColorModePicker, {
+ extends: "select",
+});
+
+class PaperSizePicker extends PrintSettingSelect {
+ initialize() {
+ super.initialize();
+ this._printerName = null;
+ this._section = this.closest(".section-block");
+ document.addEventListener("hide-paper-size", this);
+ }
+
+ update(settings) {
+ if (settings.printerName !== this._printerName) {
+ this._printerName = settings.printerName;
+ this.setOptions(settings.paperSizes);
+ }
+ this.value = settings.paperId;
+
+ // Unhide the paper-size picker, if we've stopped using the page size as paper-size.
+ if (this._section.hidden && !settings.usePageRuleSizeAsPaperSize) {
+ this._section.hidden = false;
+ }
+ }
+
+ handleEvent(e) {
+ super.handleEvent(e);
+ const { type } = e;
+ if (type == "hide-paper-size") {
+ this._section.hidden = true;
+ }
+ }
+}
+customElements.define("paper-size-select", PaperSizePicker, {
+ extends: "select",
+});
+
+class OrientationInput extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+ document.addEventListener("hide-orientation", this);
+ }
+
+ get templateId() {
+ return "orientation-template";
+ }
+
+ update(settings) {
+ for (let input of this.querySelectorAll("input")) {
+ input.checked = settings.orientation == input.value;
+ }
+ }
+
+ handleEvent(e) {
+ if (e.type == "hide-orientation") {
+ document.getElementById("orientation").hidden = true;
+ return;
+ }
+ this.dispatchSettingsChange({
+ orientation: e.target.value,
+ });
+ }
+}
+customElements.define("orientation-input", OrientationInput);
+
+class CopiesInput extends PrintUIControlMixin(HTMLElement) {
+ get templateId() {
+ return "copy-template";
+ }
+
+ initialize() {
+ super.initialize();
+ this._copiesSection = this.closest(".section-block");
+ this._copiesInput = this.querySelector("#copies-count");
+ this._copiesError = this.querySelector("#error-invalid-copies");
+ }
+
+ update(settings) {
+ this._copiesSection.hidden = settings.willSaveToFile;
+ this._copiesError.hidden = true;
+ }
+
+ handleEvent(e) {
+ this._copiesError.hidden = this._copiesInput.checkValidity();
+ }
+}
+customElements.define("copy-count-input", CopiesInput);
+
+class ScaleInput extends PrintUIControlMixin(HTMLElement) {
+ get templateId() {
+ return "scale-template";
+ }
+
+ initialize() {
+ super.initialize();
+
+ this._percentScale = this.querySelector("#percent-scale");
+ this._shrinkToFitChoice = this.querySelector("#fit-choice");
+ this._scaleChoice = this.querySelector("#percent-scale-choice");
+ this._scaleError = this.querySelector("#error-invalid-scale");
+ }
+
+ updateScale() {
+ this.dispatchSettingsChange({
+ scaling: Number(this._percentScale.value / 100),
+ });
+ }
+
+ update(settings) {
+ let { scaling, shrinkToFit, printerName } = settings;
+ this._shrinkToFitChoice.checked = shrinkToFit;
+ this._scaleChoice.checked = !shrinkToFit;
+ if (this.disableScale != shrinkToFit) {
+ this._percentScale.disabled = shrinkToFit;
+ this._percentScale.toggleAttribute("disallowed", shrinkToFit);
+ }
+ this.disableScale = shrinkToFit;
+ if (!this.printerName) {
+ this.printerName = printerName;
+ }
+
+ // If the user had an invalid input and switches back to "fit to page",
+ // we repopulate the scale field with the stored, valid scaling value.
+ let isValid = this._percentScale.checkValidity();
+ if (
+ !this._percentScale.value ||
+ (this._shrinkToFitChoice.checked && !isValid) ||
+ (this.printerName != printerName && !isValid)
+ ) {
+ // Only allow whole numbers. 0.14 * 100 would have decimal places, etc.
+ this._percentScale.value = parseInt(scaling * 100, 10);
+ this.printerName = printerName;
+ if (!isValid) {
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._scaleError.hidden = true;
+ }
+ }
+ }
+
+ handleEvent(e) {
+ if (e.target == this._shrinkToFitChoice || e.target == this._scaleChoice) {
+ if (!this._percentScale.checkValidity()) {
+ this._percentScale.value = 100;
+ }
+ let scale =
+ e.target == this._shrinkToFitChoice
+ ? 1
+ : Number(this._percentScale.value / 100);
+ this.dispatchSettingsChange({
+ shrinkToFit: this._shrinkToFitChoice.checked,
+ scaling: scale,
+ });
+ this._scaleError.hidden = true;
+ } else if (e.type == "input") {
+ if (this._percentScale.checkValidity()) {
+ this.updateScale();
+ }
+ }
+
+ window.clearTimeout(this.showErrorTimeoutId);
+ if (this._percentScale.validity.valid) {
+ this._scaleError.hidden = true;
+ } else {
+ this.cancelSettingsChange({ scaling: true });
+ this.showErrorTimeoutId = window.setTimeout(() => {
+ this._scaleError.hidden = false;
+ }, INPUT_DELAY_MS);
+ }
+ }
+}
+customElements.define("scale-input", ScaleInput);
+
+class PageRangeInput extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+
+ this._rangeInput = this.querySelector("#custom-range");
+ this._rangeInput.title = "";
+ this._rangePicker = this.querySelector("#range-picker");
+ this._rangePickerEvenOption = this._rangePicker.namedItem("even");
+ this._rangeError = this.querySelector("#error-invalid-range");
+ this._startRangeOverflowError = this.querySelector(
+ "#error-invalid-start-range-overflow"
+ );
+
+ this._pagesSet = new Set();
+
+ this.addEventListener("keypress", this);
+ this.addEventListener("paste", this);
+ document.addEventListener("page-count", this);
+ }
+
+ get templateId() {
+ return "page-range-template";
+ }
+
+ updatePageRange() {
+ let isCustom = this._rangePicker.value == "custom";
+ let isCurrent = this._rangePicker.value == "current";
+
+ if (!isCurrent) {
+ this._currentPage = null;
+ }
+
+ if (isCustom) {
+ this.validateRangeInput();
+ } else if (isCurrent) {
+ this._currentPage = this._rangeInput.value =
+ this._currentPage || this.getCurrentVisiblePageNumber();
+ this.validateRangeInput();
+ } else {
+ this._pagesSet.clear();
+
+ if (this._rangePicker.value == "odd") {
+ for (let i = 1; i <= this._numPages; i += 2) {
+ this._pagesSet.add(i);
+ }
+ } else if (this._rangePicker.value == "even") {
+ for (let i = 2; i <= this._numPages; i += 2) {
+ this._pagesSet.add(i);
+ }
+ }
+
+ if (!this._rangeInput.checkValidity()) {
+ this._rangeInput.setCustomValidity("");
+ this._rangeInput.value = "";
+ }
+ }
+
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+
+ document.l10n.setAttributes(
+ this._rangeError,
+ "printui-error-invalid-range",
+ {
+ numPages: this._numPages,
+ }
+ );
+
+ // If it's valid, update the page range and hide the error messages.
+ // Otherwise, set the appropriate error message
+ if (this._rangeInput.validity.valid || !isCustom) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ this._startRangeOverflowError.hidden = this._rangeError.hidden = true;
+ } else {
+ this._rangeInput.focus();
+ }
+ }
+
+ dispatchPageRange(shouldCancel = true) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ if (
+ this._rangeInput.validity.valid ||
+ this._rangePicker.value != "custom"
+ ) {
+ this.dispatchSettingsChange({
+ pageRanges: this.formatPageRange(),
+ });
+ } else {
+ if (shouldCancel) {
+ this.cancelSettingsChange({ pageRanges: true });
+ }
+ this.showErrorTimeoutId = window.setTimeout(() => {
+ this._rangeError.hidden =
+ this._rangeInput.validationMessage != "invalid";
+ this._startRangeOverflowError.hidden =
+ this._rangeInput.validationMessage != "startRangeOverflow";
+ }, INPUT_DELAY_MS);
+ }
+ }
+
+ // The platform expects pageRanges to be an array of
+ // ranges represented by ints.
+ // Ex: Printing pages 1-3 would return [1,3]
+ // Ex: Printing page 1 would return [1,1]
+ // Ex: Printing pages 1-2,4 would return [1,2,4,4]
+ formatPageRange() {
+ if (
+ this._pagesSet.size == 0 ||
+ (this._rangePicker.value == "custom" && this._rangeInput.value == "") ||
+ this._rangePicker.value == "all"
+ ) {
+ // Show all pages.
+ return [];
+ }
+ let pages = Array.from(this._pagesSet).sort((a, b) => a - b);
+
+ let formattedRanges = [];
+ let startRange = pages[0];
+ let endRange = pages[0];
+ formattedRanges.push(startRange);
+
+ for (let i = 1; i < pages.length; i++) {
+ let currentPage = pages[i - 1];
+ let nextPage = pages[i];
+ if (nextPage > currentPage + 1) {
+ formattedRanges.push(endRange);
+ startRange = endRange = nextPage;
+ formattedRanges.push(startRange);
+ } else {
+ endRange = nextPage;
+ }
+ }
+ formattedRanges.push(endRange);
+
+ return formattedRanges;
+ }
+
+ update(settings) {
+ let { pageRanges, printerName } = settings;
+
+ this.toggleAttribute("all-pages", !pageRanges.length);
+ if (!this.printerName) {
+ this.printerName = printerName;
+ }
+
+ let isValid = this._rangeInput.checkValidity();
+
+ if (this.printerName != printerName && !isValid) {
+ this.printerName = printerName;
+ this._rangeInput.value = "";
+ this.updatePageRange();
+ this.dispatchPageRange();
+ }
+ }
+
+ handleKeypress(e) {
+ let char = String.fromCharCode(e.charCode);
+ let acceptedChar = char.match(/^[0-9,-]$/);
+ if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ }
+ }
+
+ handlePaste(e) {
+ let paste = (e.clipboardData || window.clipboardData)
+ .getData("text")
+ .trim();
+ if (!paste.match(/^[0-9,-]*$/)) {
+ e.preventDefault();
+ }
+ }
+
+ // This method has been abstracted into a helper for testing purposes
+ _validateRangeInput(value, numPages) {
+ this._pagesSet.clear();
+ var ranges = value.split(",");
+
+ for (let range of ranges) {
+ let rangeParts = range.split("-");
+ if (rangeParts.length > 2) {
+ this._rangeInput.setCustomValidity("invalid");
+ this._rangeInput.title = "";
+ this._pagesSet.clear();
+ return;
+ }
+ let startRange = parseInt(rangeParts[0], 10);
+ let endRange = parseInt(
+ rangeParts.length == 2 ? rangeParts[1] : rangeParts[0],
+ 10
+ );
+
+ if (isNaN(startRange) && isNaN(endRange)) {
+ continue;
+ }
+
+ // If the startRange was not specified, then we infer this
+ // to be 1.
+ if (isNaN(startRange) && rangeParts[0] == "") {
+ startRange = 1;
+ }
+ // If the end range was not specified, then we infer this
+ // to be the total number of pages.
+ if (isNaN(endRange) && rangeParts[1] == "") {
+ endRange = numPages;
+ }
+
+ // Check the range for errors
+ if (endRange < startRange) {
+ this._rangeInput.setCustomValidity("startRangeOverflow");
+ this._pagesSet.clear();
+ return;
+ } else if (
+ startRange > numPages ||
+ endRange > numPages ||
+ startRange == 0
+ ) {
+ this._rangeInput.setCustomValidity("invalid");
+ this._rangeInput.title = "";
+ this._pagesSet.clear();
+ return;
+ }
+
+ for (let i = startRange; i <= endRange; i++) {
+ this._pagesSet.add(i);
+ }
+ }
+
+ this._rangeInput.setCustomValidity("");
+ }
+
+ validateRangeInput() {
+ let value = ["custom", "current"].includes(this._rangePicker.value)
+ ? this._rangeInput.value
+ : "";
+ this._validateRangeInput(value, this._numPages);
+ }
+
+ getCurrentVisiblePageNumber() {
+ let pageNum = parseInt(
+ PrintEventHandler.printPreviewEl.lastPreviewBrowser.getAttribute(
+ "current-page"
+ )
+ );
+ return isNaN(pageNum) ? 1 : pageNum;
+ }
+
+ handleEvent(e) {
+ if (e.type == "keypress") {
+ if (e.target == this._rangeInput) {
+ this.handleKeypress(e);
+ }
+ return;
+ }
+
+ if (e.type === "paste" && e.target == this._rangeInput) {
+ this.handlePaste(e);
+ return;
+ }
+
+ if (e.type == "page-count") {
+ let { totalPages } = e.detail;
+ // This means we have already handled the page count event
+ // and do not need to dispatch another event.
+ if (this._numPages == totalPages) {
+ return;
+ }
+
+ this._numPages = totalPages;
+ this._rangeInput.disabled = false;
+ this._rangePickerEvenOption.disabled = this._numPages < 2;
+
+ let prevPages = Array.from(this._pagesSet);
+ this.updatePageRange();
+ if (
+ prevPages.length != this._pagesSet.size ||
+ !prevPages.every(page => this._pagesSet.has(page))
+ ) {
+ // If the calculated set of pages has changed then we need to dispatch
+ // a new pageRanges setting :(
+ // Ideally this would be resolved in the settings code since it should
+ // only happen for the "N-" case where pages N through the end of the
+ // document are in the range.
+ this.dispatchPageRange(false);
+ }
+
+ return;
+ }
+
+ if (e.target == this._rangePicker) {
+ this._rangeInput.hidden = e.target.value != "custom";
+ this.updatePageRange();
+ this.dispatchPageRange();
+ if (!this._rangeInput.hidden) {
+ this._rangeInput.select();
+ }
+ } else if (e.target == this._rangeInput) {
+ this._rangeInput.focus();
+ if (this._numPages) {
+ this.updatePageRange();
+ this.dispatchPageRange();
+ }
+ }
+ }
+}
+customElements.define("page-range-input", PageRangeInput);
+
+class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+
+ this._marginPicker = this.querySelector("#margins-picker");
+ this._customTopMargin = this.querySelector("#custom-margin-top");
+ this._customBottomMargin = this.querySelector("#custom-margin-bottom");
+ this._customLeftMargin = this.querySelector("#custom-margin-left");
+ this._customRightMargin = this.querySelector("#custom-margin-right");
+ this._marginError = this.querySelector("#error-invalid-margin");
+ this._sizeUnit = null;
+ this._toInchesMultiplier = 1;
+ }
+
+ get templateId() {
+ return "margins-template";
+ }
+
+ updateCustomMargins() {
+ let newMargins = {
+ marginTop: this.toInchValue(this._customTopMargin.value),
+ marginBottom: this.toInchValue(this._customBottomMargin.value),
+ marginLeft: this.toInchValue(this._customLeftMargin.value),
+ marginRight: this.toInchValue(this._customRightMargin.value),
+ };
+
+ this.dispatchSettingsChange({
+ margins: "custom",
+ customMargins: newMargins,
+ });
+ this._marginError.hidden = true;
+ }
+
+ updateMaxValues() {
+ let maxWidth = this.toCurrentUnitValue(this._maxWidth);
+ let maxHeight = this.toCurrentUnitValue(this._maxHeight);
+ this._customTopMargin.max = this.formatMaxAttr(
+ maxHeight - this._customBottomMargin.value
+ );
+ this._customBottomMargin.max = this.formatMaxAttr(
+ maxHeight - this._customTopMargin.value
+ );
+ this._customLeftMargin.max = this.formatMaxAttr(
+ maxWidth - this._customRightMargin.value
+ );
+ this._customRightMargin.max = this.formatMaxAttr(
+ maxWidth - this._customLeftMargin.value
+ );
+ }
+
+ truncateTwoDecimals(val) {
+ if (val.split(".")[1].length > 2) {
+ let dotIndex = val.indexOf(".");
+ return val.slice(0, dotIndex + 3);
+ }
+ return val;
+ }
+
+ formatMaxAttr(val) {
+ const strVal = val.toString();
+ if (strVal.includes(".")) {
+ return this.truncateTwoDecimals(strVal);
+ }
+ return val;
+ }
+
+ formatMargin(target) {
+ if (target.value.includes(".")) {
+ target.value = this.truncateTwoDecimals(target.value);
+ }
+ }
+
+ toCurrentUnitValue(val) {
+ if (typeof val == "string") {
+ val = parseFloat(val);
+ }
+ return val / this._toInchesMultiplier;
+ }
+
+ toInchValue(val) {
+ if (typeof val == "string") {
+ val = parseFloat(val);
+ }
+ return val * this._toInchesMultiplier;
+ }
+
+ setAllMarginValues(settings) {
+ this._customTopMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginTop
+ ).toFixed(2);
+ this._customBottomMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginBottom
+ ).toFixed(2);
+ this._customLeftMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginLeft
+ ).toFixed(2);
+ this._customRightMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginRight
+ ).toFixed(2);
+ }
+
+ update(settings) {
+ // Re-evaluate which margin options should be enabled whenever the printer or paper changes
+ this._toInchesMultiplier =
+ settings.paperSizeUnit == settings.kPaperSizeMillimeters
+ ? INCHES_PER_MM
+ : 1;
+ if (
+ settings.paperId !== this._paperId ||
+ settings.printerName !== this._printerName ||
+ settings.orientation !== this._orientation
+ ) {
+ let enabledMargins = settings.marginOptions;
+ for (let option of this._marginPicker.options) {
+ option.hidden = !enabledMargins[option.value];
+ }
+ this._paperId = settings.paperId;
+ this._printerName = settings.printerName;
+ this._orientation = settings.orientation;
+
+ // Paper dimensions are in the paperSizeUnit. As the margin values are in inches
+ // we'll normalize to that when storing max dimensions
+ let height =
+ this._orientation == 0 ? settings.paperHeight : settings.paperWidth;
+ let width =
+ this._orientation == 0 ? settings.paperWidth : settings.paperHeight;
+ let heightInches =
+ Math.round(this._toInchesMultiplier * height * 100) / 100;
+ let widthInches =
+ Math.round(this._toInchesMultiplier * width * 100) / 100;
+
+ this._maxHeight =
+ heightInches -
+ settings.unwriteableMarginTop -
+ settings.unwriteableMarginBottom;
+ this._maxWidth =
+ widthInches -
+ settings.unwriteableMarginLeft -
+ settings.unwriteableMarginRight;
+
+ // The values in custom fields should be initialized to custom margin values
+ // and must be overriden if they are no longer valid.
+ this.setAllMarginValues(settings);
+ this.updateMaxValues();
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._marginError.hidden = true;
+ }
+
+ if (settings.paperSizeUnit !== this._sizeUnit) {
+ this._sizeUnit = settings.paperSizeUnit;
+ let unitStr =
+ this._sizeUnit == settings.kPaperSizeMillimeters ? "mm" : "inches";
+ for (let elem of this.querySelectorAll("[data-unit-prefix-l10n-id]")) {
+ let l10nId = elem.getAttribute("data-unit-prefix-l10n-id") + unitStr;
+ document.l10n.setAttributes(elem, l10nId);
+ }
+ }
+
+ // We need to ensure we don't override the value if the value should be custom.
+ if (this._marginPicker.value != "custom") {
+ // Reset the custom margin values if they are not valid and revalidate the form
+ if (
+ !this._customTopMargin.checkValidity() ||
+ !this._customBottomMargin.checkValidity() ||
+ !this._customLeftMargin.checkValidity() ||
+ !this._customRightMargin.checkValidity()
+ ) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ this.setAllMarginValues(settings);
+ this.updateMaxValues();
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._marginError.hidden = true;
+ }
+ if (settings.margins == "custom") {
+ // Ensure that we display the custom margin boxes
+ this.querySelector(".margin-group").hidden = false;
+ }
+ this._marginPicker.value = settings.margins;
+ }
+ }
+
+ handleEvent(e) {
+ if (e.target == this._marginPicker) {
+ let customMargin = e.target.value == "custom";
+ this.querySelector(".margin-group").hidden = !customMargin;
+ if (customMargin) {
+ // Update the custom margin values to ensure consistency
+ this.updateCustomMargins();
+ return;
+ }
+
+ this.dispatchSettingsChange({
+ margins: e.target.value,
+ customMargins: null,
+ });
+ }
+
+ if (
+ e.target == this._customTopMargin ||
+ e.target == this._customBottomMargin ||
+ e.target == this._customLeftMargin ||
+ e.target == this._customRightMargin
+ ) {
+ if (e.target.checkValidity()) {
+ this.updateMaxValues();
+ }
+ if (
+ this._customTopMargin.validity.valid &&
+ this._customBottomMargin.validity.valid &&
+ this._customLeftMargin.validity.valid &&
+ this._customRightMargin.validity.valid
+ ) {
+ this.formatMargin(e.target);
+ this.updateCustomMargins();
+ } else if (e.target.validity.stepMismatch) {
+ // If this is the third digit after the decimal point, we should
+ // truncate the string.
+ this.formatMargin(e.target);
+ }
+ }
+
+ window.clearTimeout(this.showErrorTimeoutId);
+ if (
+ this._customTopMargin.validity.valid &&
+ this._customBottomMargin.validity.valid &&
+ this._customLeftMargin.validity.valid &&
+ this._customRightMargin.validity.valid
+ ) {
+ this._marginError.hidden = true;
+ } else {
+ this.cancelSettingsChange({ customMargins: true, margins: true });
+ this.showErrorTimeoutId = window.setTimeout(() => {
+ this._marginError.hidden = false;
+ }, INPUT_DELAY_MS);
+ }
+ }
+}
+customElements.define("margins-select", MarginsPicker);
+
+class TwistySummary extends PrintUIControlMixin(HTMLElement) {
+ get isOpen() {
+ return this.closest("details")?.hasAttribute("open");
+ }
+
+ get templateId() {
+ return "twisty-summary-template";
+ }
+
+ initialize() {
+ if (this._initialized) {
+ return;
+ }
+ super.initialize();
+ this.label = this.querySelector(".label");
+
+ this.addEventListener("click", this);
+ let shouldOpen = Services.prefs.getBoolPref(
+ "print.more-settings.open",
+ false
+ );
+ this.closest("details").open = shouldOpen;
+ this.updateSummary(shouldOpen);
+ }
+
+ handleEvent(e) {
+ let willOpen = !this.isOpen;
+ Services.prefs.setBoolPref("print.more-settings.open", willOpen);
+ this.updateSummary(willOpen);
+ }
+
+ updateSummary(open) {
+ document.l10n.setAttributes(
+ this.label,
+ open
+ ? this.getAttribute("data-open-l10n-id")
+ : this.getAttribute("data-closed-l10n-id")
+ );
+ }
+}
+customElements.define("twisty-summary", TwistySummary);
+
+class PageCount extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+ document.addEventListener("page-count", this);
+ }
+
+ update(settings) {
+ this.numCopies = settings.numCopies;
+ this.duplex = settings.duplex;
+ this.outputDestination = settings.outputDestination;
+ this.render();
+ }
+
+ render() {
+ if (!this.numCopies || !this.sheetCount) {
+ return;
+ }
+
+ let sheetCount = this.sheetCount;
+
+ // When printing to a printer (not to a file) update
+ // the sheet count to account for duplex printing.
+ if (
+ this.outputDestination == Ci.nsIPrintSettings.kOutputDestinationPrinter &&
+ this.duplex != Ci.nsIPrintSettings.kDuplexNone
+ ) {
+ sheetCount = Math.ceil(sheetCount / 2);
+ }
+
+ sheetCount *= this.numCopies;
+
+ document.l10n.setAttributes(this, "printui-sheets-count", {
+ sheetCount,
+ });
+
+ // The loading attribute must be removed on first render
+ if (this.hasAttribute("loading")) {
+ this.removeAttribute("loading");
+ }
+
+ if (this.id) {
+ // We're showing the sheet count, so let it describe the dialog.
+ document.body.setAttribute("aria-describedby", this.id);
+ }
+ }
+
+ handleEvent(e) {
+ this.sheetCount = e.detail.sheetCount;
+ this.render();
+ }
+}
+customElements.define("page-count", PageCount);
+
+class PrintBackgrounds extends PrintSettingCheckbox {
+ update(settings) {
+ super.update(settings);
+ let isSimplified = settings.sourceVersion == "simplified";
+ this.disabled = isSimplified;
+ this.toggleAttribute("disallowed", isSimplified);
+ this.checked = !isSimplified && settings.printBackgrounds;
+ }
+}
+customElements.define("print-backgrounds", PrintBackgrounds, {
+ extends: "input",
+});
+
+class PrintButton extends PrintUIControlMixin(HTMLButtonElement) {
+ update(settings) {
+ let l10nId =
+ settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? "printui-primary-button-save"
+ : "printui-primary-button";
+ document.l10n.setAttributes(this, l10nId);
+ }
+}
+customElements.define("print-button", PrintButton, { extends: "button" });
+
+class CancelButton extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.addEventListener("click", () => {
+ this.dispatchEvent(new Event("cancel-print", { bubbles: true }));
+ });
+ }
+}
+customElements.define("cancel-button", CancelButton, { extends: "button" });
+
+async function pickFileName(contentTitle, currentURI) {
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let [title] = await document.l10n.formatMessages([
+ { id: "printui-save-to-pdf-title" },
+ ]);
+ title = title.value;
+
+ let filename;
+ if (contentTitle != "") {
+ filename = contentTitle;
+ } else {
+ let url = new URL(currentURI);
+ let path = decodeURIComponent(url.pathname);
+ path = path.replace(/\/$/, "");
+ filename = path.split("/").pop();
+ if (filename == "") {
+ filename = url.hostname;
+ }
+ }
+ if (!filename.endsWith(".pdf")) {
+ // macOS and linux don't set the extension based on the default extension.
+ // Windows won't add the extension a second time, fortunately.
+ // If it already ends with .pdf though, adding it again isn't needed.
+ filename += ".pdf";
+ }
+ filename = DownloadPaths.sanitize(filename);
+
+ picker.init(
+ window.docShell.chromeEventHandler.ownerGlobal,
+ title,
+ Ci.nsIFilePicker.modeSave
+ );
+ picker.appendFilter("PDF", "*.pdf");
+ picker.defaultExtension = "pdf";
+ picker.defaultString = filename;
+
+ let retval = await new Promise(resolve => picker.open(resolve));
+
+ if (retval == 1) {
+ throw new Error({ reason: "cancelled" });
+ } else {
+ // OK clicked (retval == 0) or replace confirmed (retval == 2)
+
+ // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
+ // the print progress listener is never called. This workaround ensures that a correct status is always returned.
+ try {
+ let fstream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
+ fstream.close();
+
+ // Remove the file to reduce the likelihood of the user opening an empty or damaged fle when the
+ // preview is loading
+ await IOUtils.remove(picker.file.path);
+ } catch (e) {
+ throw new Error({ reason: retval == 0 ? "not_saved" : "not_replaced" });
+ }
+ }
+
+ return picker.file.path;
+}