2316 lines
73 KiB
JavaScript
2316 lines
73 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 {
|
|
gBrowser,
|
|
PrintUtils,
|
|
Services,
|
|
AppConstants,
|
|
} = window.docShell.chromeEventHandler.ownerGlobal;
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"DownloadPaths",
|
|
"resource://gre/modules/DownloadPaths.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"DeferredTask",
|
|
"resource://gre/modules/DeferredTask.jsm"
|
|
);
|
|
|
|
const INPUT_DELAY_MS = 500;
|
|
const MM_PER_POINT = 25.4 / 72;
|
|
const INCHES_PER_POINT = 1 / 72;
|
|
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.import("resource://gre/modules/Console.jsm");
|
|
// 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 nameValues;
|
|
}
|
|
|
|
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 = [];
|
|
}
|
|
|
|
async function finalizeDeferredTasks() {
|
|
await Promise.all(deferredTasks.map(task => task.finalize()));
|
|
printPending = true;
|
|
await PrintEventHandler._updatePrintPreviewTask.finalize();
|
|
}
|
|
|
|
document.addEventListener(
|
|
"DOMContentLoaded",
|
|
e => {
|
|
window._initialized = PrintEventHandler.init();
|
|
ourBrowser.setAttribute("flex", "0");
|
|
ourBrowser.classList.add("printSettingsBrowser");
|
|
ourBrowser.closest(".dialogBox")?.classList.add("printDialogBox");
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
window.addEventListener("dialogclosing", () => {
|
|
PrintEventHandler.unload();
|
|
cancelDeferredTasks();
|
|
});
|
|
|
|
window.addEventListener(
|
|
"unload",
|
|
e => {
|
|
document.textContent = "";
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
var PrintEventHandler = {
|
|
settings: null,
|
|
defaultSettings: null,
|
|
allPaperSizes: {},
|
|
_nonFlaggedChangedSettings: {},
|
|
_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,
|
|
},
|
|
originalSourceContentTitle: null,
|
|
originalSourceCurrentURI: null,
|
|
previewBrowser: null,
|
|
|
|
// 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"]),
|
|
|
|
async init() {
|
|
Services.telemetry.scalarAdd("printing.preview_opened_tm", 1);
|
|
|
|
// 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.getSourceBrowsingContext();
|
|
this.previewBrowser = PrintUtils.createPreviewBrowser(
|
|
sourceBrowsingContext,
|
|
ourBrowser
|
|
);
|
|
|
|
// Get the temporary browser that will previously have been created for the
|
|
// platform code to generate the static clone printing doc into if this
|
|
// print is for a window.print() call. In that case we steal the browser's
|
|
// docshell to get the static clone, then discard it.
|
|
let args = window.arguments[0];
|
|
this.printSelectionOnly = args.getProperty("printSelectionOnly");
|
|
let existingBrowser = args.getProperty("previewBrowser");
|
|
if (existingBrowser) {
|
|
sourceBrowsingContext = existingBrowser.browsingContext;
|
|
this.previewBrowser.swapDocShells(existingBrowser);
|
|
existingBrowser.remove();
|
|
}
|
|
|
|
this.originalSourceContentTitle =
|
|
sourceBrowsingContext.currentWindowContext.documentTitle;
|
|
this.originalSourceCurrentURI =
|
|
sourceBrowsingContext.currentWindowContext.documentURI.spec;
|
|
|
|
this.printForm = document.getElementById("print");
|
|
|
|
// 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;
|
|
|
|
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
|
|
this.printForm.enable();
|
|
}
|
|
// Reset the cancel button regardless of the outcome.
|
|
document.l10n.setAttributes(
|
|
cancelButton,
|
|
cancelButton.dataset.cancelL10nId
|
|
);
|
|
});
|
|
document.addEventListener("update-print-settings", e =>
|
|
this.onUserSettingsChange(e.detail)
|
|
);
|
|
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.getSourceBrowsingContext().top.embedderElement;
|
|
let dialogBoxManager = gBrowser
|
|
.getTabDialogBox(sourceBrowser)
|
|
.getManager();
|
|
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();
|
|
settings.showPrintProgress = true;
|
|
// 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.previewBrowser.browsingContext.embedderElement.contentTitle;
|
|
const PRINTPROMPTSVC = Cc[
|
|
"@mozilla.org/embedcomp/printingprompt-service;1"
|
|
].getService(Ci.nsIPrintingPromptService);
|
|
try {
|
|
Services.telemetry.scalarAdd(
|
|
"printing.dialog_opened_via_preview_tm",
|
|
1
|
|
);
|
|
await this._showPrintDialog(PRINTPROMPTSVC, window, settings);
|
|
} catch (e) {
|
|
if (e.result == Cr.NS_ERROR_ABORT) {
|
|
Services.telemetry.scalarAdd(
|
|
"printing.dialog_via_preview_cancelled_tm",
|
|
1
|
|
);
|
|
window.close();
|
|
return; // user cancelled
|
|
}
|
|
throw e;
|
|
}
|
|
await this.print(settings);
|
|
});
|
|
|
|
let settingsToChange = await this.refreshSettings(selectedPrinter.value);
|
|
await this.updateSettings(settingsToChange, true);
|
|
|
|
// Kick off the initial print preview with the source browsing context.
|
|
let initialPreviewDone = this._updatePrintPreview(sourceBrowsingContext);
|
|
// We don't need the sourceBrowsingContext anymore, get rid of it.
|
|
sourceBrowsingContext = undefined;
|
|
|
|
// Use a DeferredTask for updating the preview. This will ensure that we
|
|
// only have one update running at a time.
|
|
this._updatePrintPreviewTask = new DeferredTask(async () => {
|
|
await initialPreviewDone;
|
|
await this._updatePrintPreview();
|
|
document.dispatchEvent(new CustomEvent("preview-updated"));
|
|
}, 0);
|
|
|
|
document.dispatchEvent(
|
|
new CustomEvent("available-destinations", {
|
|
detail: destinations,
|
|
})
|
|
);
|
|
|
|
document.dispatchEvent(
|
|
new CustomEvent("print-settings", {
|
|
detail: this.viewSettings,
|
|
})
|
|
);
|
|
|
|
await document.l10n.translateElements([this.previewBrowser]);
|
|
|
|
document.body.removeAttribute("loading");
|
|
|
|
await new Promise(resolve => window.requestAnimationFrame(resolve));
|
|
|
|
// Now that we're showing the form, select the destination select.
|
|
window.focus();
|
|
let fm = Services.focus;
|
|
fm.setFocus(document.getElementById("printer-picker"), fm.FLAG_SHOWRING);
|
|
|
|
await initialPreviewDone;
|
|
},
|
|
|
|
unload() {
|
|
this.previewBrowser.frameLoader.exitPrintPreview();
|
|
},
|
|
|
|
async print(systemDialogSettings) {
|
|
// Disable the form when a print is in progress
|
|
this.printForm.disable();
|
|
|
|
let settings = systemDialogSettings || this.settings;
|
|
|
|
if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
|
|
try {
|
|
settings.toFileName = await pickFileName(
|
|
this.originalSourceContentTitle,
|
|
this.originalSourceCurrentURI
|
|
);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
await window._initialized;
|
|
|
|
await finalizeDeferredTasks();
|
|
|
|
// This seems like it should be handled automatically but it isn't.
|
|
Services.prefs.setStringPref("print_printer", settings.printerName);
|
|
|
|
try {
|
|
// The print progress dialog is causing an uncaught exception in tests.
|
|
// Only show it to users.
|
|
this.settings.showPrintProgress = !Cu.isInAutomation;
|
|
let bc = this.previewBrowser.browsingContext;
|
|
await this._doPrint(bc, settings);
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
|
|
window.close();
|
|
return true;
|
|
},
|
|
|
|
cancelPrint() {
|
|
Services.telemetry.scalarAdd("printing.preview_cancelled_tm", 1);
|
|
window.close();
|
|
},
|
|
|
|
async refreshSettings(printerName) {
|
|
let currentPrinter;
|
|
try {
|
|
currentPrinter = await PrintSettingsViewProxy.resolvePropertiesForPrinter(
|
|
printerName
|
|
);
|
|
} catch (e) {
|
|
this.reportPrintingError("PRINTER_PROPERTIES");
|
|
throw e;
|
|
}
|
|
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.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;
|
|
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;
|
|
},
|
|
|
|
async onUserSettingsChange(changedSettings = {}) {
|
|
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 (changedSettings.printerName) {
|
|
logger.debug(
|
|
"onUserSettingsChange, changing to printerName:",
|
|
changedSettings.printerName
|
|
);
|
|
// 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(changedSettings.printerName);
|
|
} else {
|
|
changedSettings = this.getSettingsToUpdate();
|
|
}
|
|
|
|
let shouldPreviewUpdate = await this.updateSettings(
|
|
changedSettings,
|
|
!!changedSettings.printerName
|
|
);
|
|
|
|
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;
|
|
|
|
if (
|
|
parseFloat(this.viewSettings.customMargins.marginTop) +
|
|
parseFloat(this.viewSettings.customMargins.marginBottom) >
|
|
height -
|
|
paperWrapper.unwriteableMarginTop -
|
|
paperWrapper.unwriteableMarginBottom ||
|
|
this.viewSettings.customMargins.marginTop < 0 ||
|
|
this.viewSettings.customMargins.marginBottom < 0
|
|
) {
|
|
let { marginTop, marginBottom } = this.viewSettings.defaultMargins;
|
|
changedSettings.marginTop = changedSettings.customMarginTop = marginTop;
|
|
changedSettings.marginBottom = changedSettings.customMarginBottom = marginBottom;
|
|
delete this._userChangedSettings.customMargins;
|
|
}
|
|
|
|
if (
|
|
parseFloat(this.viewSettings.customMargins.marginRight) +
|
|
parseFloat(this.viewSettings.customMargins.marginLeft) >
|
|
width -
|
|
paperWrapper.unwriteableMarginRight -
|
|
paperWrapper.unwriteableMarginLeft ||
|
|
this.viewSettings.customMargins.marginLeft < 0 ||
|
|
this.viewSettings.customMargins.marginRight < 0
|
|
) {
|
|
let { marginLeft, marginRight } = this.viewSettings.defaultMargins;
|
|
changedSettings.marginLeft = changedSettings.customMarginLeft = marginLeft;
|
|
changedSettings.marginRight = changedSettings.customMarginRight = marginRight;
|
|
delete this._userChangedSettings.customMargins;
|
|
}
|
|
} 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")
|
|
) {
|
|
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.savePrintSettingsToPrefs(this.settings, true, 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._showRenderingIndicator();
|
|
this._updatePrintPreviewTask.arm();
|
|
},
|
|
|
|
/**
|
|
* Create a print preview for the provided source browsingContext, or refresh
|
|
* the preview with new settings when omitted.
|
|
*
|
|
* @param sourceBrowsingContext {BrowsingContext} [optional]
|
|
* The source BrowsingContext (the one associated with a tab or
|
|
* subdocument) that should be previewed.
|
|
*
|
|
* @return {Promise} Resolves when the preview has been updated.
|
|
*/
|
|
async _updatePrintPreview(sourceBrowsingContext) {
|
|
let { previewBrowser, settings } = this;
|
|
|
|
// We never want the progress dialog to show
|
|
settings.showPrintProgress = false;
|
|
|
|
this._showRenderingIndicator();
|
|
|
|
let sourceWinId;
|
|
if (sourceBrowsingContext) {
|
|
sourceWinId = sourceBrowsingContext.currentWindowGlobal.outerWindowId;
|
|
}
|
|
|
|
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, hasSelection;
|
|
try {
|
|
// This resolves with a PrintPreviewSuccessInfo dictionary.
|
|
({
|
|
totalPageCount,
|
|
sheetCount,
|
|
hasSelection,
|
|
} = await previewBrowser.frameLoader.printPreview(settings, sourceWinId));
|
|
} catch (e) {
|
|
this.reportPrintingError("PRINT_PREVIEW");
|
|
throw e;
|
|
}
|
|
|
|
// Update the settings print options on whether there is a selection.
|
|
settings.isPrintSelectionRBEnabled = hasSelection;
|
|
|
|
document.dispatchEvent(
|
|
new CustomEvent("page-count", {
|
|
detail: { sheetCount, totalPages: totalPageCount },
|
|
})
|
|
);
|
|
this.previewBrowser.setAttribute("sheet-count", sheetCount);
|
|
|
|
this._hideRenderingIndicator();
|
|
|
|
if (isFirstCall) {
|
|
const elapsed = Date.now() - this.printInitiationTime;
|
|
Services.telemetry
|
|
.getHistogramById("PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS")
|
|
.add(elapsed);
|
|
}
|
|
},
|
|
|
|
_showRenderingIndicator() {
|
|
let stack = this.previewBrowser.parentElement;
|
|
stack.setAttribute("rendering", true);
|
|
document.body.setAttribute("rendering", true);
|
|
},
|
|
|
|
_hideRenderingIndicator() {
|
|
let stack = this.previewBrowser.parentElement;
|
|
stack.removeAttribute("rendering");
|
|
document.body.removeAttribute("rendering");
|
|
},
|
|
|
|
getSourceBrowsingContext() {
|
|
let params = new URLSearchParams(location.search);
|
|
let browsingContextId = params.get("browsingContextId");
|
|
if (!browsingContextId) {
|
|
return null;
|
|
}
|
|
return BrowsingContext.get(browsingContextId);
|
|
},
|
|
|
|
async getPrintDestinations() {
|
|
const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
|
|
Ci.nsIPrinterList
|
|
);
|
|
let printers;
|
|
|
|
if (Cu.isInAutomation) {
|
|
printers = await Promise.resolve(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, paper) {
|
|
switch (marginSize) {
|
|
case "minimum":
|
|
return {
|
|
marginTop: paper.unwriteableMarginTop,
|
|
marginLeft: paper.unwriteableMarginLeft,
|
|
marginBottom: paper.unwriteableMarginBottom,
|
|
marginRight: paper.unwriteableMarginRight,
|
|
};
|
|
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", paper);
|
|
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) {
|
|
Services.telemetry.keyedScalarAdd("printing.error", aMessage, 1);
|
|
},
|
|
|
|
/**
|
|
* Prints the window. This method has been abstracted into a helper for
|
|
* testing purposes.
|
|
*/
|
|
_doPrint(aBrowsingContext, aSettings) {
|
|
return aBrowsingContext.top.embedderElement.print(
|
|
aBrowsingContext.currentWindowGlobal.outerWindowId,
|
|
aSettings
|
|
);
|
|
},
|
|
|
|
/**
|
|
* 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(aPrintingPromptService, aWindow, aSettings) {
|
|
return aPrintingPromptService.showPrintDialog(aWindow, 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.newPrintSettings;
|
|
printerInfo.defaultSettings.printerName = printerName;
|
|
printerInfo.defaultSettings.toFileName = "";
|
|
printerInfo.defaultSettings.outputFormat =
|
|
Ci.nsIPrintSettings.kOutputFormatPDF;
|
|
printerInfo.defaultSettings.printToFile = true;
|
|
printerInfo.paperList = this.fallbackPaperList;
|
|
}
|
|
printerInfo.settings = printerInfo.defaultSettings.clone();
|
|
// Apply any previously persisted user values
|
|
let flags = printerInfo.settings.kInitSaveAll;
|
|
if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
|
|
// Don't apply potentially-bad printToFile setting that may be in some user's prefs.
|
|
flags ^= 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;
|
|
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");
|
|
for (let presetName of ["none", "minimum", "default"]) {
|
|
let marginPresets = allMarginPresets[presetName];
|
|
if (
|
|
Object.keys(marginSettings).every(
|
|
name =>
|
|
marginSettings[name].toFixed(2) ==
|
|
marginPresets[name].toFixed(2)
|
|
)
|
|
) {
|
|
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":
|
|
return target.duplex;
|
|
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
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 "printBackgrounds":
|
|
target.printBGImages = value;
|
|
target.printBGColors = value;
|
|
break;
|
|
|
|
case "printDuplex":
|
|
target.duplex = value
|
|
? Ci.nsIPrintSettings.kDuplexHorizontal
|
|
: Ci.nsIPrintSettings.kSimplex;
|
|
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;
|
|
|
|
default:
|
|
target[name] = value;
|
|
}
|
|
},
|
|
};
|
|
|
|
/*
|
|
* Custom elements ----------------------------------------------------
|
|
*/
|
|
|
|
function PrintUIControlMixin(superClass) {
|
|
return class PrintUIControl extends superClass {
|
|
connectedCallback() {
|
|
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("change", this);
|
|
}
|
|
|
|
render() {}
|
|
|
|
update(settings) {}
|
|
|
|
dispatchSettingsChange(changedSettings) {
|
|
this.dispatchEvent(
|
|
new CustomEvent("update-print-settings", {
|
|
bubbles: true,
|
|
detail: changedSettings,
|
|
})
|
|
);
|
|
}
|
|
|
|
handleKeypress(e) {
|
|
let char = String.fromCharCode(e.charCode);
|
|
let acceptedChar = e.target.step.includes(".")
|
|
? char.match(/^[0-9.]$/)
|
|
: 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();
|
|
let acceptedChars = e.target.step.includes(".")
|
|
? paste.match(/^[0-9.]*$/)
|
|
: paste.match(/^[0-9]*$/);
|
|
if (!acceptedChars) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
handleEvent(event) {}
|
|
};
|
|
}
|
|
|
|
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 == "change" && 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 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;
|
|
this.toggleAttribute("disallowed", !canSwitch);
|
|
this.disabled = !canSwitch;
|
|
}
|
|
|
|
handleEvent(e) {
|
|
if (e.type == "change") {
|
|
// 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;
|
|
}
|
|
|
|
update(settings) {
|
|
if (settings.printerName !== this._printerName) {
|
|
this._printerName = settings.printerName;
|
|
this.setOptions(settings.paperSizes);
|
|
}
|
|
this.value = settings.paperId;
|
|
}
|
|
}
|
|
customElements.define("paper-size-select", PaperSizePicker, {
|
|
extends: "select",
|
|
});
|
|
|
|
class OrientationInput extends PrintUIControlMixin(HTMLElement) {
|
|
get templateId() {
|
|
return "orientation-template";
|
|
}
|
|
|
|
update(settings) {
|
|
for (let input of this.querySelectorAll("input")) {
|
|
input.checked = settings.orientation == input.value;
|
|
}
|
|
}
|
|
|
|
handleEvent(e) {
|
|
this.dispatchSettingsChange({
|
|
orientation: e.target.value,
|
|
});
|
|
}
|
|
}
|
|
customElements.define("orientation-input", OrientationInput);
|
|
|
|
class CopiesInput extends PrintUIControlMixin(HTMLInputElement) {
|
|
initialize() {
|
|
super.initialize();
|
|
this.addEventListener("input", this);
|
|
this.addEventListener("keypress", this);
|
|
this.addEventListener("paste", this);
|
|
}
|
|
|
|
update(settings) {
|
|
this.value = settings.numCopies;
|
|
}
|
|
|
|
handleEvent(e) {
|
|
if (e.type == "keypress") {
|
|
this.handleKeypress(e);
|
|
return;
|
|
}
|
|
|
|
if (e.type === "paste") {
|
|
this.handlePaste(e);
|
|
}
|
|
|
|
if (this.checkValidity()) {
|
|
this.dispatchSettingsChange({
|
|
numCopies: e.target.value,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
customElements.define("copy-count-input", CopiesInput, {
|
|
extends: "input",
|
|
});
|
|
|
|
class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) {
|
|
initialize() {
|
|
super.initialize();
|
|
|
|
this.addEventListener("change", this);
|
|
this.addEventListener("submit", this);
|
|
this.addEventListener("click", this);
|
|
this.addEventListener("input", this);
|
|
this.addEventListener("revalidate", this);
|
|
|
|
this._printerDestination = this.querySelector("#destination");
|
|
|
|
this.printButton = this.querySelector("#print-button");
|
|
if (AppConstants.platform != "win") {
|
|
// Move the Print button to the end if this isn't Windows.
|
|
this.printButton.parentElement.append(this.printButton);
|
|
}
|
|
this.querySelector("#pages-per-sheet").hidden = !Services.prefs.getBoolPref(
|
|
"print.pages_per_sheet.enabled",
|
|
false
|
|
);
|
|
}
|
|
|
|
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("#copies").hidden = settings.willSaveToFile;
|
|
|
|
this.querySelector("#two-sided-printing").hidden = !settings.supportsDuplex;
|
|
}
|
|
|
|
enable() {
|
|
for (let element of this.elements) {
|
|
if (!element.hasAttribute("disallowed")) {
|
|
element.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
disable() {
|
|
for (let element of this.elements) {
|
|
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 == "change" ||
|
|
e.type == "input" ||
|
|
e.type == "revalidate"
|
|
) {
|
|
let isValid = this.checkValidity();
|
|
let section = e.target.closest(".section-block");
|
|
document.body.toggleAttribute("invalid", !isValid);
|
|
if (isValid) {
|
|
// 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 {
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
customElements.define("print-form", PrintUIForm, { extends: "form" });
|
|
|
|
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");
|
|
|
|
this._percentScale.addEventListener("input", this);
|
|
this._percentScale.addEventListener("keypress", this);
|
|
this._percentScale.addEventListener("paste", this);
|
|
this.addEventListener("input", this);
|
|
|
|
this._updateScaleTask = createDeferredTask(
|
|
() => this.updateScale(),
|
|
INPUT_DELAY_MS
|
|
);
|
|
}
|
|
|
|
updateScale() {
|
|
this.dispatchSettingsChange({
|
|
scaling: Number(this._percentScale.value / 100),
|
|
});
|
|
}
|
|
|
|
update(settings) {
|
|
let { scaling, shrinkToFit, printerName } = settings;
|
|
this._shrinkToFitChoice.checked = shrinkToFit;
|
|
this._scaleChoice.checked = !shrinkToFit;
|
|
this._percentScale.disabled = shrinkToFit;
|
|
this._percentScale.toggleAttribute("disallowed", 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.type == "change") {
|
|
// We listen to input events, no need for change too.
|
|
return;
|
|
}
|
|
|
|
if (e.type == "keypress") {
|
|
this.handleKeypress(e);
|
|
return;
|
|
}
|
|
|
|
if (e.type === "paste") {
|
|
this.handlePaste(e);
|
|
}
|
|
|
|
this._updateScaleTask.disarm();
|
|
|
|
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._updateScaleTask.arm();
|
|
}
|
|
}
|
|
|
|
window.clearTimeout(this.showErrorTimeoutId);
|
|
if (this._percentScale.validity.valid) {
|
|
this._scaleError.hidden = true;
|
|
} else {
|
|
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._startRange = this.querySelector("#custom-range-start");
|
|
this._endRange = this.querySelector("#custom-range-end");
|
|
this._rangePicker = this.querySelector("#range-picker");
|
|
this._rangeError = this.querySelector("#error-invalid-range");
|
|
this._startRangeOverflowError = this.querySelector(
|
|
"#error-invalid-start-range-overflow"
|
|
);
|
|
|
|
this._updatePageRangeTask = createDeferredTask(
|
|
() => this.updatePageRange(),
|
|
INPUT_DELAY_MS
|
|
);
|
|
|
|
this.addEventListener("input", this);
|
|
this.addEventListener("keypress", this);
|
|
this.addEventListener("paste", this);
|
|
document.addEventListener("page-count", this);
|
|
}
|
|
|
|
get templateId() {
|
|
return "page-range-template";
|
|
}
|
|
|
|
updatePageRange() {
|
|
this.dispatchSettingsChange({
|
|
pageRanges: this._rangePicker.value
|
|
? [this._startRange.value, this._endRange.value]
|
|
: [],
|
|
});
|
|
}
|
|
|
|
update(settings) {
|
|
this.toggleAttribute("all-pages", !settings.pageRanges.length);
|
|
}
|
|
|
|
handleEvent(e) {
|
|
if (e.type == "keypress") {
|
|
if (e.target == this._startRange || e.target == this._endRange) {
|
|
this.handleKeypress(e);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (
|
|
e.type === "paste" &&
|
|
(e.target == this._startRange || e.target == this._endRange)
|
|
) {
|
|
this.handlePaste(e);
|
|
}
|
|
|
|
this._updatePageRangeTask.disarm();
|
|
|
|
if (e.type == "page-count") {
|
|
let { totalPages } = e.detail;
|
|
this._startRange.max = this._endRange.max = this._totalPages = totalPages;
|
|
this._startRange.disabled = this._endRange.disabled = false;
|
|
let isChanged = false;
|
|
|
|
// Changing certain settings (like orientation, scale or printer) can
|
|
// change the number of pages. We need to update the start and end rages
|
|
// if their values are no longer valid.
|
|
if (!this._startRange.checkValidity()) {
|
|
this._startRange.value = this._totalPages;
|
|
isChanged = true;
|
|
}
|
|
if (!this._endRange.checkValidity()) {
|
|
this._endRange.value = this._totalPages;
|
|
isChanged = true;
|
|
}
|
|
if (isChanged) {
|
|
window.clearTimeout(this.showErrorTimeoutId);
|
|
this._startRange.max = Math.min(this._endRange.value, totalPages);
|
|
this._endRange.min = Math.max(this._startRange.value, 1);
|
|
|
|
this.dispatchEvent(new Event("revalidate", { bubbles: true }));
|
|
|
|
if (this._startRange.validity.valid && this._endRange.validity.valid) {
|
|
this.dispatchSettingsChange({
|
|
pageRanges: [this._startRange.value, this._endRange.value],
|
|
});
|
|
this._rangeError.hidden = true;
|
|
this._startRangeOverflowError.hidden = true;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (e.target == this._rangePicker) {
|
|
let printAll = e.target.value == "all";
|
|
this._startRange.required = this._endRange.required = !printAll;
|
|
this.querySelector(".range-group").hidden = printAll;
|
|
this._startRange.value = 1;
|
|
this._endRange.value = this._totalPages || 1;
|
|
|
|
this.updatePageRange();
|
|
|
|
window.clearTimeout(this.showErrorTimeoutId);
|
|
this._rangeError.hidden = true;
|
|
this._startRangeOverflowError.hidden = true;
|
|
return;
|
|
}
|
|
|
|
if (e.target == this._startRange || e.target == this._endRange) {
|
|
if (this._startRange.checkValidity()) {
|
|
this._endRange.min = this._startRange.value;
|
|
}
|
|
if (this._endRange.checkValidity()) {
|
|
this._startRange.max = this._endRange.value;
|
|
}
|
|
if (this._startRange.checkValidity() && this._endRange.checkValidity()) {
|
|
if (this._startRange.value && this._endRange.value) {
|
|
// Update the page range after a short delay so we don't update
|
|
// multiple times as the user types a multi-digit number or uses
|
|
// up/down/mouse wheel.
|
|
this._updatePageRangeTask.arm();
|
|
}
|
|
}
|
|
}
|
|
document.l10n.setAttributes(
|
|
this._rangeError,
|
|
"printui-error-invalid-range",
|
|
{
|
|
numPages: this._totalPages,
|
|
}
|
|
);
|
|
|
|
window.clearTimeout(this.showErrorTimeoutId);
|
|
let hasShownOverflowError = false;
|
|
let startValidity = this._startRange.validity;
|
|
let endValidity = this._endRange.validity;
|
|
|
|
// Display the startRangeOverflowError if the start range exceeds
|
|
// the end range. This means either the start range is greater than its
|
|
// max constraint, whiich is determined by the end range, or the end range
|
|
// is less than its minimum constraint, determined by the start range.
|
|
if (
|
|
!(
|
|
(startValidity.rangeOverflow && endValidity.valid) ||
|
|
(endValidity.rangeUnderflow && startValidity.valid)
|
|
)
|
|
) {
|
|
this._startRangeOverflowError.hidden = true;
|
|
} else {
|
|
hasShownOverflowError = true;
|
|
this.showErrorTimeoutId = window.setTimeout(() => {
|
|
this._startRangeOverflowError.hidden = false;
|
|
}, INPUT_DELAY_MS);
|
|
}
|
|
|
|
// Display the generic error if the startRangeOverflowError is not already
|
|
// showing and a range input is invalid.
|
|
if (hasShownOverflowError || (startValidity.valid && endValidity.valid)) {
|
|
this._rangeError.hidden = true;
|
|
} else {
|
|
this.showErrorTimeoutId = window.setTimeout(() => {
|
|
this._rangeError.hidden = false;
|
|
}, INPUT_DELAY_MS);
|
|
}
|
|
}
|
|
}
|
|
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._updateCustomMarginsTask = createDeferredTask(
|
|
() => this.updateCustomMargins(),
|
|
INPUT_DELAY_MS
|
|
);
|
|
|
|
this.addEventListener("input", this);
|
|
this.addEventListener("keypress", this);
|
|
this.addEventListener("paste", this);
|
|
}
|
|
|
|
get templateId() {
|
|
return "margins-template";
|
|
}
|
|
|
|
updateCustomMargins() {
|
|
let newMargins = {
|
|
marginTop: this._customTopMargin.value,
|
|
marginBottom: this._customBottomMargin.value,
|
|
marginLeft: this._customLeftMargin.value,
|
|
marginRight: this._customRightMargin.value,
|
|
};
|
|
|
|
this.dispatchSettingsChange({
|
|
margins: "custom",
|
|
customMargins: newMargins,
|
|
});
|
|
this._marginError.hidden = true;
|
|
}
|
|
|
|
updateMaxValues() {
|
|
this._customTopMargin.max =
|
|
this._maxHeight - this._customBottomMargin.value;
|
|
this._customBottomMargin.max =
|
|
this._maxHeight - this._customTopMargin.value;
|
|
this._customLeftMargin.max = this._maxWidth - this._customRightMargin.value;
|
|
this._customRightMargin.max = this._maxWidth - this._customLeftMargin.value;
|
|
}
|
|
|
|
formatMargin(target) {
|
|
if (target.value.includes(".")) {
|
|
if (target.value.split(".")[1].length > 2) {
|
|
let dotIndex = target.value.indexOf(".");
|
|
target.value = target.value.slice(0, dotIndex + 3);
|
|
}
|
|
}
|
|
}
|
|
|
|
setAllMarginValues(settings) {
|
|
this._customTopMargin.value = parseFloat(
|
|
settings.customMargins.marginTop
|
|
).toFixed(2);
|
|
this._customBottomMargin.value = parseFloat(
|
|
settings.customMargins.marginBottom
|
|
).toFixed(2);
|
|
this._customLeftMargin.value = parseFloat(
|
|
settings.customMargins.marginLeft
|
|
).toFixed(2);
|
|
this._customRightMargin.value = parseFloat(
|
|
settings.customMargins.marginRight
|
|
).toFixed(2);
|
|
}
|
|
|
|
update(settings) {
|
|
// Re-evaluate which margin options should be enabled whenever the printer or paper changes
|
|
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;
|
|
|
|
let height =
|
|
this._orientation == 0 ? settings.paperHeight : settings.paperWidth;
|
|
let width =
|
|
this._orientation == 0 ? settings.paperWidth : settings.paperHeight;
|
|
|
|
this._maxHeight =
|
|
height -
|
|
settings.unwriteableMarginTop -
|
|
settings.unwriteableMarginBottom;
|
|
this._maxWidth =
|
|
width -
|
|
settings.unwriteableMarginLeft -
|
|
settings.unwriteableMarginRight;
|
|
|
|
this._defaultPresets = settings.defaultMargins;
|
|
// 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.dispatchEvent(new Event("revalidate", { bubbles: true }));
|
|
this._marginError.hidden = true;
|
|
}
|
|
|
|
// 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.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;
|
|
}
|
|
|
|
this.updateMaxValues();
|
|
}
|
|
|
|
handleEvent(e) {
|
|
if (e.type == "change") {
|
|
// We handle input events rather than change events, make sure we only
|
|
// dispatch one settings change per user change.
|
|
return;
|
|
}
|
|
|
|
if (e.type == "keypress") {
|
|
this.handleKeypress(e);
|
|
return;
|
|
}
|
|
|
|
if (e.type === "paste") {
|
|
this.handlePaste(e);
|
|
}
|
|
|
|
this._updateCustomMarginsTask.disarm();
|
|
|
|
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._updateCustomMarginsTask.arm();
|
|
} 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.showErrorTimeoutId = window.setTimeout(() => {
|
|
this._marginError.hidden = false;
|
|
}, INPUT_DELAY_MS);
|
|
}
|
|
}
|
|
}
|
|
customElements.define("margins-select", MarginsPicker);
|
|
|
|
class PrintSettingNumber extends PrintUIControlMixin(HTMLInputElement) {
|
|
connectedCallback() {
|
|
this.type = "number";
|
|
this.settingName = this.dataset.settingName;
|
|
super.connectedCallback();
|
|
}
|
|
update(settings) {
|
|
this.value = settings[this.settingName];
|
|
}
|
|
|
|
handleEvent(e) {
|
|
this.dispatchSettingsChange({
|
|
[this.settingName]: this.value,
|
|
});
|
|
}
|
|
}
|
|
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 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);
|
|
this.updateSummary();
|
|
}
|
|
|
|
handleEvent(e) {
|
|
let willOpen = !this.isOpen;
|
|
this.updateSummary(willOpen);
|
|
}
|
|
|
|
updateSummary(open = false) {
|
|
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.render();
|
|
}
|
|
|
|
render() {
|
|
if (!this.numCopies || !this.sheetCount) {
|
|
return;
|
|
}
|
|
document.l10n.setAttributes(this, "printui-sheets-count", {
|
|
sheetCount: this.sheetCount * this.numCopies,
|
|
});
|
|
|
|
// 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 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();
|
|
} catch (e) {
|
|
throw new Error({ reason: retval == 0 ? "not_saved" : "not_replaced" });
|
|
}
|
|
}
|
|
|
|
return picker.file.path;
|
|
}
|