2828 lines
89 KiB
JavaScript
2828 lines
89 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
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) {
|
|
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",
|
|
() => {
|
|
const dialogBox = ourBrowser.closest(".dialogBox");
|
|
if (!dialogBox) {
|
|
return;
|
|
}
|
|
|
|
window._initialized = PrintEventHandler.init().catch(e => console.error(e));
|
|
ourBrowser.setAttribute("flex", "0");
|
|
ourBrowser.setAttribute("constrainpopups", "false");
|
|
ourBrowser.classList.add("printSettingsBrowser");
|
|
dialogBox.classList.add("printDialogBox");
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
window.addEventListener("dialogclosing", () => {
|
|
cancelDeferredTasks();
|
|
});
|
|
|
|
window.addEventListener(
|
|
"unload",
|
|
() => {
|
|
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() {
|
|
Glean.printing.previewOpenedTm.add(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;
|
|
Glean.printing.dialogOpenedViaPreviewTm.add(1);
|
|
const doPrint = await this._showPrintDialog(
|
|
window,
|
|
this.hasSelection,
|
|
settings
|
|
);
|
|
if (!doPrint) {
|
|
Glean.printing.dialogViaPreviewCancelledTm.add(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() {
|
|
Glean.printing.previewCancelledTm.add(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)) {
|
|
Glean.printing.settingsChanged[setting].add(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;
|
|
|
|
let totalPageCount, sheetCount, isEmpty, orientation, pageWidth, pageHeight;
|
|
try {
|
|
// This resolves with a PrintPreviewSuccessInfo dictionary.
|
|
let { sourceVersion } = this.viewSettings;
|
|
let sourceURI = this.activeURI;
|
|
// The printing backend can't generate a title for the selection document
|
|
// since it is only a fragment of the page, give it the active title.
|
|
settings.title =
|
|
this.viewSettings.sourceVersion == "selection" ? this.activeTitle : "";
|
|
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 },
|
|
})
|
|
);
|
|
},
|
|
|
|
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);
|
|
Glean.printing.error[aMessage].add(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() {}
|
|
|
|
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() {}
|
|
};
|
|
}
|
|
|
|
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() {
|
|
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() {
|
|
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() {
|
|
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() {
|
|
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.browsingContext,
|
|
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;
|
|
}
|