/* 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; }