summaryrefslogtreecommitdiffstats
path: root/toolkit/components/printing/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/printing/content')
-rw-r--r--toolkit/components/printing/content/print.css299
-rw-r--r--toolkit/components/printing/content/print.html587
-rw-r--r--toolkit/components/printing/content/print.js2847
-rw-r--r--toolkit/components/printing/content/printPageSetup.js538
-rw-r--r--toolkit/components/printing/content/printPageSetup.xhtml291
-rw-r--r--toolkit/components/printing/content/printPagination.css141
-rw-r--r--toolkit/components/printing/content/printPreviewPagination.js185
-rw-r--r--toolkit/components/printing/content/printUtils.js822
-rw-r--r--toolkit/components/printing/content/simplifyMode.css36
-rw-r--r--toolkit/components/printing/content/toggle-group.css79
10 files changed, 5825 insertions, 0 deletions
diff --git a/toolkit/components/printing/content/print.css b/toolkit/components/printing/content/print.css
new file mode 100644
index 0000000000..2788312576
--- /dev/null
+++ b/toolkit/components/printing/content/print.css
@@ -0,0 +1,299 @@
+/* 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/. */
+
+html, body {
+ height: 100vh;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow: hidden;
+}
+
+body[loading] #print {
+ visibility: hidden;
+}
+
+*[hidden] {
+ display: none !important;
+}
+
+.section-block {
+ margin: 16px;
+}
+
+.section-block .block-label {
+ display: block;
+ margin-bottom: 8px;
+}
+
+.row {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ min-height: 1.8em;
+ margin-block: 2px;
+}
+
+.row.cols-2 {
+ flex-direction: row;
+ align-items: center;
+}
+
+#scale .row.cols-2,
+#more-settings-options > .row.cols-2 {
+ /* The margin-block of the checkboxes/radiobuttons
+ already provide the needed vertical spacing. */
+ margin-block: 0;
+}
+
+hr {
+ margin-inline: 8px;
+ margin-block: 0;
+}
+
+.header-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ flex: 0 1 auto;
+ padding: 8px 18px;
+}
+.header-container > h2 {
+ margin: 0;
+ font-size: 17px;
+ line-height: 1;
+}
+
+#sheet-count {
+ font-size: 11px;
+ padding: 3px 8px;
+ margin: 0;
+}
+
+#sheet-count[loading],
+body[invalid] #sheet-count {
+ visibility: hidden;
+}
+
+form#print {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow: hidden;
+ font: menu;
+}
+
+select {
+ margin: 0;
+}
+
+select:not([size], [multiple])[iconic] {
+ padding-inline-start: calc(8px + 16px + 4px); /* spacing before image + image width + spacing after image */
+ background-size: auto 12px, 16px;
+ background-position: right 19px center, left 8px center;
+}
+
+select:not([size], [multiple])[iconic]:dir(rtl) {
+ background-position-x: left 19px, right 8px;
+}
+
+#printer-picker {
+ background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/print.svg");
+ width: 100%;
+}
+
+#printer-picker[output="pdf"] {
+ background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/page-portrait.svg");
+}
+
+input[type="number"],
+input[type="text"] {
+ padding: 2px;
+ padding-inline-start: 8px;
+}
+
+.toggle-group-label {
+ padding: 4px 8px;
+}
+
+.body-container {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+.twisty > summary {
+ list-style: none;
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+}
+
+#more-settings > summary > .twisty {
+ background-image: url("chrome://global/skin/icons/arrow-down-12.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 12px;
+ height: 12px;
+}
+
+#more-settings > summary > .label {
+ flex-grow: 1;
+}
+
+#more-settings[open] > summary > .twisty {
+ background-image: url("chrome://global/skin/icons/arrow-up-12.svg");
+}
+
+#open-dialog-link {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#open-dialog-link::after {
+ display: block;
+ flex-shrink: 0;
+ content: "";
+ background: url(chrome://global/skin/icons/open-in-new.svg) no-repeat center;
+ background-size: 12px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 12px;
+ height: 12px;
+}
+
+#open-dialog-link:dir(rtl)::after {
+ scale: -1 1;
+}
+
+.footer-container {
+ flex: 0 1 auto;
+}
+
+#print-progress {
+ background-image: url("chrome://global/skin/icons/loading.png");
+ background-repeat: no-repeat;
+ background-size: 16px;
+ background-position: left center;
+ padding-inline-start: 20px;
+}
+
+@media (min-resolution: 1.1dppx) {
+ #print-progress {
+ background-image: url("chrome://global/skin/icons/loading@2x.png");
+ }
+}
+
+#print-progress:dir(rtl) {
+ background-position-x: right;
+}
+
+#custom-range {
+ margin-top: 4px;
+ margin-inline: 0;
+}
+
+.vertical-margins,
+.horizontal-margins {
+ display: flex;
+ gap: 20px;
+ margin-block: 5px;
+}
+
+.margin-pair {
+ width: calc(50% - 10px); /* Half minus the gap */
+}
+
+.margin-input {
+ /* FIXME: Important is needed to override the photon-number styles below,
+ * probably should be refactored */
+ width: 100% !important;
+ box-sizing: border-box;
+}
+
+.margin-descriptor {
+ text-align: center;
+ display: block;
+ margin-top: 2px;
+}
+
+.toggle-group #landscape + .toggle-group-label::before {
+ content: url("chrome://global/skin/icons/page-landscape.svg");
+}
+.toggle-group #portrait + .toggle-group-label::before {
+ content: url("chrome://global/skin/icons/page-portrait.svg");
+}
+
+.error-message {
+ font-size: 12px;
+ color: var(--in-content-error-text-color, color-mix(in srgb, currentColor 40%, #C50042));
+ margin-top: 4px;
+}
+
+#percent-scale {
+ margin-inline-start: 4px;
+}
+
+#percent-scale:disabled {
+ /* Let clicks on the disabled input select the radio button */
+ pointer-events: none;
+}
+
+input[type="number"].photon-number {
+ padding: 0;
+ padding-inline-start: 8px;
+ margin: 0;
+ height: 1.5em;
+ width: 4em;
+}
+
+input[type="number"].photon-number::-moz-number-spin-box {
+ height: 100%;
+ max-height: 100%;
+ border-inline-start: 1px solid var(--in-content-box-border-color);
+ width: 1em;
+ border-start-end-radius: 4px;
+ border-end-end-radius: 4px;
+}
+
+input[type="number"].photon-number:hover::-moz-number-spin-box {
+ border-color: var(--in-content-border-hover);
+}
+
+input[type="number"].photon-number::-moz-number-spin-up,
+input[type="number"].photon-number::-moz-number-spin-down {
+ border: 0;
+ border-radius: 0;
+ background-color: var(--in-content-button-background);
+ background-image: url("chrome://global/skin/icons/arrow-down.svg");
+ background-repeat: no-repeat;
+ background-size: 8px;
+ background-position: center;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ appearance: none;
+}
+
+input[type="number"].photon-number::-moz-number-spin-up {
+ background-image: url("chrome://global/skin/icons/arrow-up.svg");
+}
+
+input[type="number"].photon-number::-moz-number-spin-up:hover,
+input[type="number"].photon-number::-moz-number-spin-down:hover {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+}
+
+input[type="checkbox"]:disabled + label,
+input[type="radio"]:disabled + label,
+input[type="radio"]:disabled + span > label {
+ opacity: 0.5;
+}
diff --git a/toolkit/components/printing/content/print.html b/toolkit/components/printing/content/print.html
new file mode 100644
index 0000000000..62d214608c
--- /dev/null
+++ b/toolkit/components/printing/content/print.html
@@ -0,0 +1,587 @@
+<!DOCTYPE html>
+<!-- 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/. -->
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title data-l10n-id="printui-title"></title>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:;img-src data:; object-src 'none'"
+ />
+
+ <link rel="localization" href="toolkit/printing/printUI.ftl" />
+
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link rel="stylesheet" href="chrome://global/content/toggle-group.css" />
+ <link rel="stylesheet" href="chrome://global/content/print.css" />
+ <script defer src="chrome://global/content/print.js"></script>
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-button-group.mjs"
+ ></script>
+ </head>
+
+ <body loading rendering>
+ <template id="page-range-template">
+ <select
+ id="range-picker"
+ name="page-range-type"
+ data-l10n-id="printui-page-range-picker"
+ is="setting-select"
+ >
+ <option
+ value="all"
+ selected
+ data-l10n-id="printui-page-range-all"
+ ></option>
+ <option
+ value="current"
+ data-l10n-id="printui-page-range-current"
+ ></option>
+ <option value="odd" data-l10n-id="printui-page-range-odd"></option>
+ <option
+ value="even"
+ name="even"
+ data-l10n-id="printui-page-range-even"
+ ></option>
+ <option
+ value="custom"
+ data-l10n-id="printui-page-range-custom"
+ ></option>
+ </select>
+ <input
+ id="custom-range"
+ type="text"
+ disabled
+ hidden
+ data-l10n-id="printui-page-custom-range-input"
+ aria-errormessage="error-invalid-range error-invalid-start-range-overflow"
+ />
+ <p
+ id="error-invalid-range"
+ hidden
+ data-l10n-id="printui-error-invalid-range"
+ class="error-message"
+ role="alert"
+ data-l10n-args='{ "numPages": 1 }'
+ ></p>
+ <p
+ id="error-invalid-start-range-overflow"
+ hidden
+ data-l10n-id="printui-error-invalid-start-overflow"
+ class="error-message"
+ role="alert"
+ ></p>
+ </template>
+
+ <template id="orientation-template">
+ <input
+ type="radio"
+ name="orientation"
+ id="portrait"
+ value="0"
+ checked
+ class="toggle-group-input"
+ />
+ <label
+ for="portrait"
+ data-l10n-id="printui-portrait"
+ class="toggle-group-label toggle-group-label-iconic"
+ ></label>
+ <input
+ type="radio"
+ name="orientation"
+ id="landscape"
+ value="1"
+ class="toggle-group-input"
+ />
+ <label
+ for="landscape"
+ data-l10n-id="printui-landscape"
+ class="toggle-group-label toggle-group-label-iconic"
+ ></label>
+ </template>
+
+ <template id="twisty-summary-template">
+ <span class="label"></span>
+ <span class="twisty"></span>
+ </template>
+
+ <template id="scale-template">
+ <div role="radiogroup" aria-labelledby="scale-label">
+ <label class="row cols-2">
+ <input
+ type="radio"
+ name="scale-choice"
+ id="fit-choice"
+ value="fit"
+ checked
+ />
+ <span
+ data-l10n-id="printui-scale-fit-to-page-width"
+ class="col"
+ ></span>
+ </label>
+ <label class="row cols-2" for="percent-scale-choice">
+ <input type="radio" name="scale-choice" id="percent-scale-choice" />
+ <span class="col">
+ <span
+ id="percent-scale-label"
+ data-l10n-id="printui-scale-pcent"
+ ></span>
+ <!-- Note that here and elsewhere, we're setting aria-errormessage
+ attributes to a list of all possible errors. The a11y APIs
+ will filter this down to visible items only. -->
+ <input
+ id="percent-scale"
+ class="photon-number"
+ is="setting-number"
+ min="10"
+ max="200"
+ step="1"
+ size="6"
+ aria-labelledby="percent-scale-label"
+ aria-errormessage="error-invalid-scale"
+ disabled
+ required
+ />
+ </span>
+ </label>
+ <p
+ id="error-invalid-scale"
+ hidden
+ data-l10n-id="printui-error-invalid-scale"
+ class="error-message"
+ role="alert"
+ ></p>
+ </div>
+ </template>
+
+ <template id="copy-template">
+ <input
+ id="copies-count"
+ is="setting-number"
+ data-setting-name="numCopies"
+ min="1"
+ max="10000"
+ class="copy-count-input photon-number"
+ aria-errormessage="error-invalid-copies"
+ required
+ />
+ <p
+ id="error-invalid-copies"
+ hidden
+ data-l10n-id="printui-error-invalid-copies"
+ class="error-message"
+ role="alert"
+ ></p>
+ </template>
+
+ <template id="margins-template">
+ <label
+ for="margins-picker"
+ class="block-label"
+ data-l10n-id="printui-margins"
+ ></label>
+ <select
+ is="margins-select"
+ id="margins-picker"
+ name="margin-type"
+ class="row"
+ data-setting-name="margins"
+ >
+ <option value="default" data-l10n-id="printui-margins-default"></option>
+ <option value="minimum" data-l10n-id="printui-margins-min"></option>
+ <option value="none" data-l10n-id="printui-margins-none"></option>
+ <option
+ value="custom"
+ data-unit-prefix-l10n-id="printui-margins-custom-"
+ ></option>
+ </select>
+ <div id="custom-margins" class="margin-group" role="group" hidden>
+ <div class="vertical-margins">
+ <div class="margin-pair">
+ <input
+ is="setting-number"
+ id="custom-margin-top"
+ class="margin-input photon-number"
+ aria-describedby="margins-custom-margin-top-desc"
+ min="0"
+ step="0.01"
+ required
+ />
+ <label
+ for="custom-margin-top"
+ class="margin-descriptor"
+ data-l10n-id="printui-margins-custom-top"
+ ></label>
+ <label
+ hidden
+ id="margins-custom-margin-top-desc"
+ data-unit-prefix-l10n-id="printui-margins-custom-top-"
+ ></label>
+ </div>
+ <div class="margin-pair">
+ <input
+ is="setting-number"
+ id="custom-margin-bottom"
+ class="margin-input photon-number"
+ aria-describedby="margins-custom-margin-bottom-desc"
+ min="0"
+ step="0.01"
+ required
+ />
+ <label
+ for="custom-margin-bottom"
+ class="margin-descriptor"
+ data-l10n-id="printui-margins-custom-bottom"
+ ></label>
+ <label
+ hidden
+ id="margins-custom-margin-bottom-desc"
+ data-unit-prefix-l10n-id="printui-margins-custom-bottom-"
+ ></label>
+ </div>
+ </div>
+ <div class="horizontal-margins">
+ <div class="margin-pair">
+ <input
+ is="setting-number"
+ id="custom-margin-left"
+ class="margin-input photon-number"
+ aria-describedby="margins-custom-margin-left-desc"
+ min="0"
+ step="0.01"
+ required
+ />
+ <label
+ for="custom-margin-left"
+ class="margin-descriptor"
+ data-l10n-id="printui-margins-custom-left"
+ ></label>
+ <label
+ hidden
+ id="margins-custom-margin-left-desc"
+ data-unit-prefix-l10n-id="printui-margins-custom-left-"
+ ></label>
+ </div>
+ <div class="margin-pair">
+ <input
+ is="setting-number"
+ id="custom-margin-right"
+ class="margin-input photon-number"
+ aria-describedby="margins-custom-margin-right-desc"
+ min="0"
+ step="0.01"
+ required
+ />
+ <label
+ for="custom-margin-right"
+ class="margin-descriptor"
+ data-l10n-id="printui-margins-custom-right"
+ ></label>
+ <label
+ hidden
+ id="margins-custom-margin-right-desc"
+ data-unit-prefix-l10n-id="printui-margins-custom-right-"
+ ></label>
+ </div>
+ </div>
+ <p
+ id="error-invalid-margin"
+ hidden
+ data-l10n-id="printui-error-invalid-margin"
+ class="error-message"
+ role="alert"
+ ></p>
+ </div>
+ </template>
+
+ <header class="header-container" role="none">
+ <h2 data-l10n-id="printui-title"></h2>
+ <div aria-live="off">
+ <p
+ id="sheet-count"
+ is="page-count"
+ data-l10n-id="printui-sheets-count"
+ data-l10n-args='{ "sheetCount": 0 }'
+ loading
+ ></p>
+ </div>
+ </header>
+
+ <hr />
+
+ <form id="print" is="print-form" aria-labelledby="page-header">
+ <section class="body-container">
+ <section id="destination" class="section-block">
+ <label
+ for="printer-picker"
+ class="block-label"
+ data-l10n-id="printui-destination-label"
+ ></label>
+ <div class="printer-picker-wrapper">
+ <select
+ is="destination-picker"
+ id="printer-picker"
+ data-setting-name="printerName"
+ iconic
+ ></select>
+ </div>
+ </section>
+ <section id="settings">
+ <section id="copies" class="section-block">
+ <label
+ for="copies-count"
+ class="block-label"
+ data-l10n-id="printui-copies-label"
+ ></label>
+ <copy-count-input></copy-count-input>
+ </section>
+
+ <section id="orientation" class="section-block">
+ <label
+ id="orientation-label"
+ class="block-label"
+ data-l10n-id="printui-orientation"
+ ></label>
+ <div
+ is="orientation-input"
+ class="toggle-group"
+ role="radiogroup"
+ aria-labelledby="orientation-label"
+ ></div>
+ </section>
+
+ <section id="pages" class="section-block">
+ <label
+ for="page-range-input"
+ class="block-label"
+ data-l10n-id="printui-page-range-label"
+ ></label>
+ <div
+ id="page-range-input"
+ is="page-range-input"
+ class="page-range-input row"
+ ></div>
+ </section>
+
+ <section id="color-mode" class="section-block">
+ <label
+ for="color-mode-picker"
+ class="block-label"
+ data-l10n-id="printui-color-mode-label"
+ ></label>
+ <select
+ is="color-mode-select"
+ id="color-mode-picker"
+ class="row"
+ data-setting-name="printInColor"
+ >
+ <option
+ value="color"
+ selected
+ data-l10n-id="printui-color-mode-color"
+ ></option>
+ <option value="bw" data-l10n-id="printui-color-mode-bw"></option>
+ </select>
+ </section>
+
+ <hr />
+
+ <details id="more-settings" class="twisty">
+ <summary
+ class="block-label section-block"
+ is="twisty-summary"
+ data-open-l10n-id="printui-less-settings"
+ data-closed-l10n-id="printui-more-settings"
+ ></summary>
+
+ <section id="paper-size" class="section-block">
+ <label
+ for="paper-size-picker"
+ class="block-label"
+ data-l10n-id="printui-paper-size-label"
+ ></label>
+ <select
+ is="paper-size-select"
+ id="paper-size-picker"
+ class="row"
+ data-setting-name="paperId"
+ ></select>
+ </section>
+
+ <section id="scale" class="section-block">
+ <label
+ id="scale-label"
+ class="block-label"
+ data-l10n-id="printui-scale"
+ ></label>
+ <scale-input></scale-input>
+ </section>
+
+ <section id="pages-per-sheet" class="section-block">
+ <label
+ id="pages-per-sheet-label"
+ for="pages-per-sheet-picker"
+ class="block-label"
+ data-l10n-id="printui-pages-per-sheet"
+ ></label>
+ <select
+ is="setting-select"
+ id="pages-per-sheet-picker"
+ class="row"
+ data-setting-name="numPagesPerSheet"
+ >
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="4">4</option>
+ <option value="6">6</option>
+ <option value="9">9</option>
+ <option value="16">16</option>
+ </select>
+ </section>
+
+ <section id="margins" class="section-block">
+ <div
+ id="margins-select"
+ is="margins-select"
+ class="margins-select row"
+ ></div>
+ </section>
+
+ <section id="two-sided-printing" class="section-block">
+ <label
+ class="block-label"
+ for="duplex-select"
+ data-l10n-id="printui-two-sided-printing"
+ ></label>
+ <select
+ is="setting-select"
+ id="duplex-select"
+ name="duplex-type"
+ class="row"
+ data-setting-name="printDuplex"
+ >
+ <option
+ value="off"
+ data-l10n-id="printui-two-sided-printing-off"
+ selected
+ ></option>
+ <option
+ value="long-edge"
+ data-l10n-id="printui-two-sided-printing-long-edge"
+ ></option>
+ <option
+ value="short-edge"
+ data-l10n-id="printui-two-sided-printing-short-edge"
+ ></option>
+ </select>
+ </section>
+
+ <section id="source-version-section" class="section-block">
+ <label
+ id="source-version-label"
+ data-l10n-id="printui-source-label"
+ ></label>
+ <div role="radiogroup" aria-labelledby="source-version-label">
+ <label id="source-version-source" class="row cols-2">
+ <input
+ is="setting-radio"
+ name="source-version"
+ value="source"
+ id="source-version-source-radio"
+ data-setting-name="sourceVersion"
+ checked
+ />
+ <span data-l10n-id="printui-source-radio"></span>
+ </label>
+ <label id="source-version-selection" class="row cols-2">
+ <input
+ is="setting-radio"
+ name="source-version"
+ value="selection"
+ id="source-version-selection-radio"
+ data-setting-name="sourceVersion"
+ />
+ <span data-l10n-id="printui-selection-radio"></span>
+ </label>
+ <label id="source-version-simplified" class="row cols-2">
+ <input
+ is="setting-radio"
+ name="source-version"
+ value="simplified"
+ id="source-version-simplified-radio"
+ data-setting-name="sourceVersion"
+ />
+ <span data-l10n-id="printui-simplify-page-radio"></span>
+ </label>
+ </div>
+ </section>
+
+ <section id="more-settings-options" class="section-block">
+ <label class="block-label" data-l10n-id="printui-options"></label>
+ <label id="headers-footers" class="row cols-2">
+ <input
+ is="setting-checkbox"
+ id="headers-footers-enabled"
+ data-setting-name="printFootersHeaders"
+ />
+ <span data-l10n-id="printui-headers-footers-checkbox"></span>
+ </label>
+ <label id="backgrounds" class="row cols-2">
+ <input
+ is="print-backgrounds"
+ id="backgrounds-enabled"
+ data-setting-name="printBackgrounds"
+ />
+ <span data-l10n-id="printui-backgrounds-checkbox"></span>
+ </label>
+ </section>
+ </details>
+ </section>
+
+ <section id="system-print" class="section-block">
+ <a
+ href="#"
+ id="open-dialog-link"
+ data-l10n-id="printui-system-dialog-link"
+ ></a>
+ </section>
+ </section>
+
+ <hr />
+
+ <footer class="footer-container" id="print-footer" role="none">
+ <p
+ id="print-progress"
+ class="section-block"
+ data-l10n-id="printui-print-progress-indicator"
+ hidden
+ ></p>
+ <moz-button-group id="button-container" class="section-block">
+ <button
+ id="print-button"
+ class="primary"
+ showfocus
+ name="print"
+ data-l10n-id="printui-primary-button"
+ is="print-button"
+ type="submit"
+ ></button>
+ <button
+ id="cancel-button"
+ name="cancel"
+ data-l10n-id="printui-cancel-button"
+ data-close-l10n-id="printui-close-button"
+ data-cancel-l10n-id="printui-cancel-button"
+ type="button"
+ is="cancel-button"
+ ></button>
+ </moz-button-group>
+ </footer>
+ </form>
+ </body>
+</html>
diff --git a/toolkit/components/printing/content/print.js b/toolkit/components/printing/content/print.js
new file mode 100644
index 0000000000..45938431b5
--- /dev/null
+++ b/toolkit/components/printing/content/print.js
@@ -0,0 +1,2847 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { PrintUtils, Services, AppConstants } =
+ window.docShell.chromeEventHandler.ownerGlobal;
+
+ChromeUtils.defineESModuleGetters(this, {
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
+});
+
+const PDF_JS_URI = "resource://pdf.js/web/viewer.html";
+const INPUT_DELAY_MS = Cu.isInAutomation ? 100 : 500;
+const MM_PER_POINT = 25.4 / 72;
+const INCHES_PER_POINT = 1 / 72;
+const INCHES_PER_MM = 1 / 25.4;
+const ourBrowser = window.docShell.chromeEventHandler;
+const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+);
+
+var logger = (function () {
+ const getMaxLogLevel = () =>
+ Services.prefs.getBoolPref("print.debug", false) ? "all" : "warn";
+
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+ let _logger = new ConsoleAPI({
+ prefix: "printUI",
+ maxLogLevel: getMaxLogLevel(),
+ });
+
+ function onPrefChange() {
+ if (_logger) {
+ _logger.maxLogLevel = getMaxLogLevel();
+ }
+ }
+ // Watch for pref changes and the maxLogLevel for the logger
+ Services.prefs.addObserver("print.debug", onPrefChange);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("print.debug", onPrefChange);
+ });
+ return _logger;
+})();
+
+function serializeSettings(settings, logPrefix) {
+ let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception?
+ let types = new Set(["string", "boolean", "number", "undefined"]);
+ let nameValues = {};
+ for (let key in settings) {
+ try {
+ if (!re.test(key) && types.has(typeof settings[key])) {
+ nameValues[key] = settings[key];
+ }
+ } catch (e) {
+ logger.warn("Exception accessing setting: ", key, e);
+ }
+ }
+ return JSON.stringify(nameValues, null, 2);
+}
+
+let printPending = false;
+let deferredTasks = [];
+function createDeferredTask(fn, timeout) {
+ let task = new DeferredTask(fn, timeout);
+ deferredTasks.push(task);
+ return task;
+}
+
+function cancelDeferredTasks() {
+ for (let task of deferredTasks) {
+ task.disarm();
+ }
+ PrintEventHandler._updatePrintPreviewTask?.disarm();
+ deferredTasks = [];
+}
+
+document.addEventListener(
+ "DOMContentLoaded",
+ e => {
+ window._initialized = PrintEventHandler.init().catch(e => console.error(e));
+ ourBrowser.setAttribute("flex", "0");
+ ourBrowser.setAttribute("constrainpopups", "false");
+ ourBrowser.classList.add("printSettingsBrowser");
+ ourBrowser.closest(".dialogBox")?.classList.add("printDialogBox");
+ },
+ { once: true }
+);
+
+window.addEventListener("dialogclosing", () => {
+ cancelDeferredTasks();
+});
+
+window.addEventListener(
+ "unload",
+ e => {
+ document.textContent = "";
+ },
+ { once: true }
+);
+
+var PrintEventHandler = {
+ settings: null,
+ defaultSettings: null,
+ allPaperSizes: {},
+ previewIsEmpty: false,
+ _delayedChanges: {},
+ _userChangedSettings: {},
+ settingFlags: {
+ margins: Ci.nsIPrintSettings.kInitSaveMargins,
+ customMargins: Ci.nsIPrintSettings.kInitSaveMargins,
+ orientation: Ci.nsIPrintSettings.kInitSaveOrientation,
+ paperId:
+ Ci.nsIPrintSettings.kInitSavePaperSize |
+ Ci.nsIPrintSettings.kInitSaveUnwriteableMargins,
+ printInColor: Ci.nsIPrintSettings.kInitSaveInColor,
+ scaling: Ci.nsIPrintSettings.kInitSaveScaling,
+ shrinkToFit: Ci.nsIPrintSettings.kInitSaveShrinkToFit,
+ printDuplex: Ci.nsIPrintSettings.kInitSaveDuplex,
+ printFootersHeaders:
+ Ci.nsIPrintSettings.kInitSaveHeaderLeft |
+ Ci.nsIPrintSettings.kInitSaveHeaderCenter |
+ Ci.nsIPrintSettings.kInitSaveHeaderRight |
+ Ci.nsIPrintSettings.kInitSaveFooterLeft |
+ Ci.nsIPrintSettings.kInitSaveFooterCenter |
+ Ci.nsIPrintSettings.kInitSaveFooterRight,
+ printBackgrounds:
+ Ci.nsIPrintSettings.kInitSaveBGColors |
+ Ci.nsIPrintSettings.kInitSaveBGImages,
+ },
+
+ topContentTitle: null,
+ topCurrentURI: null,
+ activeContentTitle: null,
+ activeCurrentURI: null,
+
+ get activeURI() {
+ return this.viewSettings.sourceVersion == "selection"
+ ? this.activeCurrentURI
+ : this.topCurrentURI;
+ },
+ get activeTitle() {
+ return this.viewSettings.sourceVersion == "selection"
+ ? this.activeContentTitle
+ : this.topContentTitle;
+ },
+
+ // These settings do not have an associated pref value or flag, but
+ // changing them requires us to update the print preview.
+ _nonFlaggedUpdatePreviewSettings: new Set([
+ "pageRanges",
+ "numPagesPerSheet",
+ "sourceVersion",
+ ]),
+ _noPreviewUpdateSettings: new Set(["numCopies", "printDuplex"]),
+
+ async init() {
+ Services.telemetry.scalarAdd("printing.preview_opened_tm", 1);
+
+ this.printPreviewEl =
+ ourBrowser.parentElement.querySelector("print-preview");
+
+ // Do not keep a reference to source browser, it may mutate after printing
+ // is initiated and the print preview clone must be a snapshot from the
+ // time that the print was started.
+ let sourceBrowsingContext = this.printPreviewEl.getSourceBrowsingContext();
+
+ let args = window.arguments[0];
+ this.printFrameOnly = args.getProperty("printFrameOnly");
+ this.printSelectionOnly = args.getProperty("printSelectionOnly");
+ this.isArticle = args.getProperty("isArticle");
+ this.hasSelection = await PrintUtils.checkForSelection(
+ sourceBrowsingContext
+ );
+
+ let sourcePrincipal =
+ sourceBrowsingContext.currentWindowGlobal.documentPrincipal;
+ let sourceIsPdf =
+ !sourcePrincipal.isNullPrincipal && sourcePrincipal.spec == PDF_JS_URI;
+ this.activeContentTitle =
+ sourceBrowsingContext.currentWindowContext.documentTitle;
+ this.activeCurrentURI =
+ sourceBrowsingContext.currentWindowContext.documentURI.spec;
+ let topWindowContext = sourceBrowsingContext.top.currentWindowContext;
+ this.topContentTitle = topWindowContext.documentTitle;
+ this.topCurrentURI = topWindowContext.documentURI.spec;
+ this.isReader = this.topCurrentURI.startsWith("about:reader");
+
+ let canSimplify = !this.isReader && this.isArticle;
+ if (!this.hasSelection && !canSimplify) {
+ document.getElementById("source-version-section").hidden = true;
+ } else {
+ document.getElementById("source-version-selection").hidden =
+ !this.hasSelection;
+ document.getElementById("source-version-simplified").hidden =
+ !canSimplify;
+ }
+
+ // We don't need the sourceBrowsingContext anymore, get rid of it.
+ sourceBrowsingContext = undefined;
+
+ this.printProgressIndicator = document.getElementById("print-progress");
+ this.printForm = document.getElementById("print");
+ if (sourceIsPdf) {
+ this.printForm.removeNonPdfSettings();
+ }
+
+ // Let the dialog appear before doing any potential main thread work.
+ await ourBrowser._dialogReady;
+
+ // First check the available destinations to ensure we get settings for an
+ // accessible printer.
+ let destinations,
+ defaultSystemPrinter,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName;
+ try {
+ ({
+ destinations,
+ defaultSystemPrinter,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName,
+ } = await this.getPrintDestinations());
+ } catch (e) {
+ this.reportPrintingError("PRINT_DESTINATIONS");
+ throw e;
+ }
+ PrintSettingsViewProxy.availablePrinters = printersByName;
+ PrintSettingsViewProxy.fallbackPaperList = fallbackPaperList;
+ PrintSettingsViewProxy.defaultSystemPrinter = defaultSystemPrinter;
+ PrintSettingsViewProxy._sourceVersion =
+ this.hasSelection && this.printSelectionOnly ? "selection" : "source";
+
+ logger.debug("availablePrinters: ", Object.keys(printersByName));
+ logger.debug("defaultSystemPrinter: ", defaultSystemPrinter);
+
+ document.addEventListener("print", async () => {
+ let cancelButton = document.getElementById("cancel-button");
+ document.l10n.setAttributes(
+ cancelButton,
+ cancelButton.dataset.closeL10nId
+ );
+ let didPrint = await this.print();
+ if (!didPrint) {
+ // Re-enable elements of the form if the user cancels saving or
+ // if a deferred task rendered the page invalid.
+ this.printForm.enable();
+ }
+ // Reset the cancel button regardless of the outcome.
+ document.l10n.setAttributes(
+ cancelButton,
+ cancelButton.dataset.cancelL10nId
+ );
+ });
+ this._createDelayedSettingsChangeTask();
+ document.addEventListener("update-print-settings", e => {
+ this.handleSettingsChange(e.detail);
+ });
+ document.addEventListener("cancel-print-settings", e => {
+ this._delayedSettingsChangeTask.disarm();
+ for (let setting of Object.keys(e.detail)) {
+ delete this._delayedChanges[setting];
+ }
+ });
+ document.addEventListener("cancel-print", () => this.cancelPrint());
+ document.addEventListener("open-system-dialog", async () => {
+ // This file in only used if pref print.always_print_silent is false, so
+ // no need to check that here.
+
+ // Hide the dialog box before opening system dialog
+ // We cannot close the window yet because the browsing context for the
+ // print preview browser is needed to print the page.
+ let sourceBrowser =
+ this.printPreviewEl.getSourceBrowsingContext().top.embedderElement;
+ let dialogBoxManager =
+ PrintUtils.getTabDialogBox(sourceBrowser).getTabDialogManager();
+ dialogBoxManager.hideDialog(sourceBrowser);
+
+ // Use our settings to prepopulate the system dialog.
+ // The system print dialog won't recognize our internal save-to-pdf
+ // pseudo-printer. We need to pass it a settings object from any
+ // system recognized printer.
+ let settings =
+ this.settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? PrintUtils.getPrintSettings(this.viewSettings.defaultSystemPrinter)
+ : this.settings.clone();
+ // We set the title so that if the user chooses save-to-PDF from the
+ // system dialog the title will be used to generate the prepopulated
+ // filename in the file picker.
+ settings.title = this.activeTitle;
+ Services.telemetry.scalarAdd("printing.dialog_opened_via_preview_tm", 1);
+ const doPrint = await this._showPrintDialog(
+ window,
+ this.hasSelection,
+ settings
+ );
+ if (!doPrint) {
+ Services.telemetry.scalarAdd(
+ "printing.dialog_via_preview_cancelled_tm",
+ 1
+ );
+ window.close();
+ return;
+ }
+ await this.print(settings);
+ });
+
+ let originalError;
+ const printersByPriority = [
+ selectedPrinter.value,
+ ...Object.getOwnPropertyNames(printersByName).filter(
+ name => name != selectedPrinter.value
+ ),
+ ];
+
+ // Try to update settings, falling back to any available printer
+ for (const printerName of printersByPriority) {
+ try {
+ let settingsToChange = await this.refreshSettings(printerName);
+ await this.updateSettings(settingsToChange, true);
+ originalError = null;
+ break;
+ } catch (e) {
+ if (!originalError) {
+ originalError = e;
+ // Report on how often fetching the last used printer settings fails.
+ this.reportPrintingError("PRINTER_SETTINGS_LAST_USED");
+ }
+ }
+ }
+
+ // Only throw original error if no fallback was possible
+ if (originalError) {
+ this.reportPrintingError("PRINTER_SETTINGS");
+ throw originalError;
+ }
+
+ let initialPreviewDone = this._updatePrintPreview();
+
+ // Use a DeferredTask for updating the preview. This will ensure that we
+ // only have one update running at a time.
+ this._createUpdatePrintPreviewTask(initialPreviewDone);
+
+ document.dispatchEvent(
+ new CustomEvent("available-destinations", {
+ detail: destinations,
+ })
+ );
+
+ document.dispatchEvent(
+ new CustomEvent("print-settings", {
+ detail: this.viewSettings,
+ })
+ );
+
+ document.body.removeAttribute("loading");
+
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ // Now that we're showing the form, select the destination select.
+ document.getElementById("printer-picker").focus({ focusVisible: true });
+
+ await initialPreviewDone;
+ },
+
+ async print(systemDialogSettings) {
+ // Disable the form when a print is in progress
+ this.printForm.disable();
+
+ if (Object.keys(this._delayedChanges).length) {
+ // Make sure any pending changes get saved.
+ let task = this._delayedSettingsChangeTask;
+ this._createDelayedSettingsChangeTask();
+ await task.finalize();
+ }
+
+ if (this.settings.pageRanges.length) {
+ // Finish any running previews to verify the range is still valid.
+ let task = this._updatePrintPreviewTask;
+ this._createUpdatePrintPreviewTask();
+ await task.finalize();
+ }
+
+ if (!this.printForm.checkValidity() || this.previewIsEmpty) {
+ return false;
+ }
+
+ let settings = systemDialogSettings || this.settings;
+
+ if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ try {
+ settings.toFileName = await pickFileName(
+ this.activeTitle,
+ this.activeURI
+ );
+ } catch (e) {
+ return false;
+ }
+ }
+
+ await window._initialized;
+
+ // This seems like it should be handled automatically but it isn't.
+ PSSVC.maybeSaveLastUsedPrinterNameToPrefs(settings.printerName);
+
+ try {
+ // We'll provide our own progress indicator.
+ let l10nId =
+ settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? "printui-print-progress-indicator-saving"
+ : "printui-print-progress-indicator";
+ document.l10n.setAttributes(this.printProgressIndicator, l10nId);
+ this.printProgressIndicator.hidden = false;
+ let bc = this.printPreviewEl.currentBrowsingContext;
+ await this._doPrint(bc, settings);
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ // Clear the file name from the preference value since it may potentially
+ // contain sensitive information from the page title (Bug 1675965)
+ let prefName =
+ "print.printer_" +
+ settings.printerName.replace(/ /g, "_") +
+ ".print_to_filename";
+ Services.prefs.clearUserPref(prefName);
+ }
+
+ window.close();
+ return true;
+ },
+
+ /**
+ * Prints the window. This method has been abstracted into a helper for
+ * testing purposes.
+ */
+ _doPrint(aBrowsingContext, aSettings) {
+ return aBrowsingContext.print(aSettings);
+ },
+
+ cancelPrint() {
+ Services.telemetry.scalarAdd("printing.preview_cancelled_tm", 1);
+ window.close();
+ },
+
+ async refreshSettings(printerName) {
+ this.currentPrinterName = printerName;
+ let currentPrinter;
+ try {
+ currentPrinter = await PrintSettingsViewProxy.resolvePropertiesForPrinter(
+ printerName
+ );
+ } catch (e) {
+ this.reportPrintingError("PRINTER_PROPERTIES");
+ throw e;
+ }
+ if (this.currentPrinterName != printerName) {
+ // Refresh settings could take a while, if the destination has changed
+ // then we don't want to update the settings after all.
+ return {};
+ }
+
+ this.settings = currentPrinter.settings;
+ this.defaultSettings = currentPrinter.defaultSettings;
+
+ this.settings.printSelectionOnly = this.printSelectionOnly;
+
+ logger.debug("currentPrinter name: ", printerName);
+ logger.debug("settings:", serializeSettings(this.settings));
+
+ // Some settings are only used by the UI
+ // assigning new values should update the underlying settings
+ this.viewSettings = new Proxy(this.settings, PrintSettingsViewProxy);
+ return this.getSettingsToUpdate();
+ },
+
+ getSettingsToUpdate() {
+ // Get the previously-changed settings we want to try to use on this printer
+ let settingsToUpdate = Object.assign({}, this._userChangedSettings);
+
+ // Ensure the color option is correct, if either of the supportsX flags are
+ // false then the user cannot change the value through the UI.
+ if (!this.viewSettings.supportsColor) {
+ settingsToUpdate.printInColor = false;
+ } else if (!this.viewSettings.supportsMonochrome) {
+ settingsToUpdate.printInColor = true;
+ }
+
+ if (settingsToUpdate.sourceVersion == "simplified") {
+ if (this.viewSettings.printBackgrounds) {
+ // Remember that this was true before so it gets restored if the
+ // format is changed to something else.
+ this._userChangedSettings.printBackgrounds = true;
+ }
+ // Backgrounds are removed in simplified mode and this setting changes
+ // the output subtly to be less legible.
+ settingsToUpdate.printBackgrounds = false;
+ }
+
+ if (
+ settingsToUpdate.printInColor != this._userChangedSettings.printInColor
+ ) {
+ delete this._userChangedSettings.printInColor;
+ }
+
+ // See if the paperId needs to change.
+ let paperId = settingsToUpdate.paperId || this.viewSettings.paperId;
+
+ logger.debug("Using paperId: ", paperId);
+ logger.debug(
+ "Available paper sizes: ",
+ PrintSettingsViewProxy.availablePaperSizes
+ );
+ let matchedPaper =
+ paperId && PrintSettingsViewProxy.availablePaperSizes[paperId];
+ if (!matchedPaper) {
+ let paperWidth, paperHeight, paperSizeUnit;
+ if (settingsToUpdate.paperId) {
+ // The user changed paperId in this instance and session,
+ // We should have details on the paper size from the previous printer
+ paperId = settingsToUpdate.paperId;
+ let cachedPaperWrapper = this.allPaperSizes[paperId];
+ // for the purposes of finding a best-size match, we'll use mm
+ paperWidth = cachedPaperWrapper.paper.width * MM_PER_POINT;
+ paperHeight = cachedPaperWrapper.paper.height * MM_PER_POINT;
+ paperSizeUnit = PrintEventHandler.settings.kPaperSizeMillimeters;
+ } else {
+ paperId = this.viewSettings.paperId;
+ logger.debug(
+ "No paperId or matchedPaper, get a new default from viewSettings:",
+ paperId
+ );
+ paperWidth = this.viewSettings.paperWidth;
+ paperHeight = this.viewSettings.paperHeight;
+ paperSizeUnit = this.viewSettings.paperSizeUnit;
+ }
+ matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
+ paperWidth,
+ paperHeight,
+ paperSizeUnit
+ );
+ }
+ if (!matchedPaper) {
+ // We didn't find a good match. Take the first paper size
+ matchedPaper = Object.values(
+ PrintSettingsViewProxy.availablePaperSizes
+ )[0];
+ delete this._userChangedSettings.paperId;
+ }
+ if (matchedPaper.id !== paperId) {
+ // The exact paper id doesn't exist for this printer
+ logger.log(
+ `Requested paperId: "${paperId}" missing on this printer, using: ${matchedPaper.id} instead`
+ );
+ delete this._userChangedSettings.paperId;
+ }
+ // Always write paper details back to settings
+ settingsToUpdate.paperId = matchedPaper.id;
+
+ return settingsToUpdate;
+ },
+
+ _createDelayedSettingsChangeTask() {
+ this._delayedSettingsChangeTask = createDeferredTask(async () => {
+ if (Object.keys(this._delayedChanges).length) {
+ let changes = this._delayedChanges;
+ this._delayedChanges = {};
+ await this.onUserSettingsChange(changes);
+ }
+ }, INPUT_DELAY_MS);
+ },
+
+ _createUpdatePrintPreviewTask(initialPreviewDone = null) {
+ this._updatePrintPreviewTask = new DeferredTask(async () => {
+ await initialPreviewDone;
+ await this._updatePrintPreview();
+ document.dispatchEvent(new CustomEvent("preview-updated"));
+ }, 0);
+ },
+
+ _scheduleDelayedSettingsChange(changes) {
+ Object.assign(this._delayedChanges, changes);
+ this._delayedSettingsChangeTask.disarm();
+ this._delayedSettingsChangeTask.arm();
+ },
+
+ handleSettingsChange(changedSettings = {}) {
+ let delayedChanges = {};
+ let instantChanges = {};
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ switch (setting) {
+ case "pageRanges":
+ case "scaling":
+ delayedChanges[setting] = value;
+ break;
+ case "customMargins":
+ delete this._delayedChanges.margins;
+ changedSettings.margins == "custom"
+ ? (delayedChanges[setting] = value)
+ : (instantChanges[setting] = value);
+ break;
+ default:
+ instantChanges[setting] = value;
+ break;
+ }
+ }
+ if (Object.keys(delayedChanges).length) {
+ this._scheduleDelayedSettingsChange(delayedChanges);
+ }
+ if (Object.keys(instantChanges).length) {
+ this.onUserSettingsChange(instantChanges);
+ }
+ },
+
+ async onUserSettingsChange(changedSettings = {}) {
+ let previewableChange = false;
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ Services.telemetry.keyedScalarAdd(
+ "printing.settings_changed",
+ setting,
+ 1
+ );
+ // Update the list of user-changed settings, which we attempt to maintain
+ // across printer changes.
+ this._userChangedSettings[setting] = value;
+ if (!this._noPreviewUpdateSettings.has(setting)) {
+ previewableChange = true;
+ }
+ }
+ if (changedSettings.printerName) {
+ logger.debug(
+ "onUserSettingsChange, changing to printerName:",
+ changedSettings.printerName
+ );
+ this.printForm.printerChanging = true;
+ this.printForm.disable(el => el.id != "printer-picker");
+ let { printerName } = changedSettings;
+ // Treat a printerName change separately, because it involves a settings
+ // object switch and we don't want to set the new name on the old settings.
+ changedSettings = await this.refreshSettings(printerName);
+ if (printerName != this.currentPrinterName) {
+ // Don't continue this update if the printer changed again.
+ return;
+ }
+ this.printForm.printerChanging = false;
+ this.printForm.enable();
+ } else {
+ changedSettings = this.getSettingsToUpdate();
+ }
+
+ let shouldPreviewUpdate =
+ (await this.updateSettings(
+ changedSettings,
+ !!changedSettings.printerName
+ )) && previewableChange;
+
+ if (shouldPreviewUpdate && !printPending) {
+ // We do not need to arm the preview task if the user has already printed
+ // and finalized any deferred tasks.
+ this.updatePrintPreview();
+ }
+ document.dispatchEvent(
+ new CustomEvent("print-settings", {
+ detail: this.viewSettings,
+ })
+ );
+ },
+
+ async updateSettings(changedSettings = {}, printerChanged = false) {
+ let updatePreviewWithoutFlag = false;
+ let flags = 0;
+ logger.debug("updateSettings ", changedSettings, printerChanged);
+
+ if (printerChanged || changedSettings.paperId) {
+ // The paper's margin properties are async,
+ // so resolve those now before we update the settings
+ try {
+ let paperWrapper = await PrintSettingsViewProxy.fetchPaperMargins(
+ changedSettings.paperId || this.viewSettings.paperId
+ );
+
+ // See if we also need to change the custom margin values
+
+ let paperHeightInInches = paperWrapper.paper.height * INCHES_PER_POINT;
+ let paperWidthInInches = paperWrapper.paper.width * INCHES_PER_POINT;
+ let height =
+ (changedSettings.orientation || this.viewSettings.orientation) == 0
+ ? paperHeightInInches
+ : paperWidthInInches;
+ let width =
+ (changedSettings.orientation || this.viewSettings.orientation) == 0
+ ? paperWidthInInches
+ : paperHeightInInches;
+
+ function verticalMarginsInvalid(margins) {
+ return (
+ parseFloat(margins.marginTop) + parseFloat(margins.marginBottom) >
+ height -
+ paperWrapper.unwriteableMarginTop -
+ paperWrapper.unwriteableMarginBottom
+ );
+ }
+
+ function horizontalMarginsInvalid(margins) {
+ return (
+ parseFloat(margins.marginRight) + parseFloat(margins.marginLeft) >
+ width -
+ paperWrapper.unwriteableMarginRight -
+ paperWrapper.unwriteableMarginLeft
+ );
+ }
+
+ let unwriteableMarginsInvalid = false;
+ if (
+ verticalMarginsInvalid(this.viewSettings.customMargins) ||
+ this.viewSettings.customMargins.marginTop < 0 ||
+ this.viewSettings.customMargins.marginBottom < 0
+ ) {
+ let { marginTop, marginBottom } = this.viewSettings.defaultMargins;
+ if (verticalMarginsInvalid(this.viewSettings.defaultMargins)) {
+ let marginsNone = this.getMarginPresets("none");
+ marginTop = marginsNone.marginTop;
+ marginBottom = marginsNone.marginBottom;
+ unwriteableMarginsInvalid = true;
+ }
+ changedSettings.marginTop = changedSettings.customMarginTop =
+ marginTop;
+ changedSettings.marginBottom = changedSettings.customMarginBottom =
+ marginBottom;
+ delete this._userChangedSettings.customMargins;
+ }
+
+ if (
+ horizontalMarginsInvalid(this.viewSettings.customMargins) ||
+ this.viewSettings.customMargins.marginLeft < 0 ||
+ this.viewSettings.customMargins.marginRight < 0
+ ) {
+ let { marginLeft, marginRight } = this.viewSettings.defaultMargins;
+ if (horizontalMarginsInvalid(this.viewSettings.defaultMargins)) {
+ let marginsNone = this.getMarginPresets("none");
+ marginLeft = marginsNone.marginLeft;
+ marginRight = marginsNone.marginRight;
+ unwriteableMarginsInvalid = true;
+ }
+ changedSettings.marginLeft = changedSettings.customMarginLeft =
+ marginLeft;
+ changedSettings.marginRight = changedSettings.customMarginRight =
+ marginRight;
+ delete this._userChangedSettings.customMargins;
+ }
+
+ if (unwriteableMarginsInvalid) {
+ changedSettings.ignoreUnwriteableMargins = true;
+ }
+ } catch (e) {
+ this.reportPrintingError("PAPER_MARGINS");
+ throw e;
+ }
+ }
+
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ // Always write paper changes back to settings as pref-derived values could be bad
+ if (
+ this.viewSettings[setting] != value ||
+ (printerChanged && setting == "paperId")
+ ) {
+ if (setting == "pageRanges") {
+ // The page range is kept as an array. If the user switches between all
+ // and custom with no specified range input (which is represented as an
+ // empty array), we do not want to send an update.
+ if (!this.viewSettings[setting].length && !value.length) {
+ continue;
+ }
+ }
+ this.viewSettings[setting] = value;
+
+ if (
+ setting in this.settingFlags &&
+ setting in this._userChangedSettings
+ ) {
+ flags |= this.settingFlags[setting];
+ }
+ updatePreviewWithoutFlag |=
+ this._nonFlaggedUpdatePreviewSettings.has(setting);
+ }
+ }
+
+ let shouldPreviewUpdate =
+ flags || printerChanged || updatePreviewWithoutFlag;
+ logger.debug(
+ "updateSettings, calculated flags:",
+ flags,
+ "shouldPreviewUpdate:",
+ shouldPreviewUpdate
+ );
+ if (flags) {
+ this.saveSettingsToPrefs(flags);
+ }
+ return shouldPreviewUpdate;
+ },
+
+ saveSettingsToPrefs(flags) {
+ PSSVC.maybeSavePrintSettingsToPrefs(this.settings, flags);
+ },
+
+ /**
+ * Queue a task to update the print preview. It will start immediately or when
+ * the in progress update completes.
+ */
+ async updatePrintPreview() {
+ // Make sure the rendering state is set so we don't visibly update the
+ // sheet count with incomplete data.
+ this._updatePrintPreviewTask.arm();
+ },
+
+ /**
+ * Creates a print preview or refreshes the preview with new settings when omitted.
+ *
+ * @return {Promise} Resolves when the preview has been updated.
+ */
+ async _updatePrintPreview() {
+ let { settings } = this;
+
+ const isFirstCall = !this.printInitiationTime;
+ if (isFirstCall) {
+ let params = new URLSearchParams(location.search);
+ this.printInitiationTime = parseInt(
+ params.get("printInitiationTime"),
+ 10
+ );
+ const elapsed = Date.now() - this.printInitiationTime;
+ Services.telemetry
+ .getHistogramById("PRINT_INIT_TO_PLATFORM_SENT_SETTINGS_MS")
+ .add(elapsed);
+ }
+
+ let totalPageCount, sheetCount, isEmpty, orientation, pageWidth, pageHeight;
+ try {
+ // This resolves with a PrintPreviewSuccessInfo dictionary.
+ let { sourceVersion } = this.viewSettings;
+ let sourceURI = this.activeURI;
+ this._lastPrintPreviewSettings = settings;
+ ({
+ totalPageCount,
+ sheetCount,
+ isEmpty,
+ orientation,
+ pageWidth,
+ pageHeight,
+ } = await this.printPreviewEl.printPreview(settings, {
+ sourceVersion,
+ sourceURI,
+ }));
+ } catch (e) {
+ this.reportPrintingError("PRINT_PREVIEW");
+ console.error(e);
+ throw e;
+ }
+
+ // If there is a set orientation, update the settings to use it. In this
+ // case, the document will already have used this orientation to create
+ // the print preview.
+ if (orientation != "unspecified") {
+ const kIPrintSettings = Ci.nsIPrintSettings;
+ settings.orientation =
+ orientation == "landscape"
+ ? kIPrintSettings.kLandscapeOrientation
+ : kIPrintSettings.kPortraitOrientation;
+ document.dispatchEvent(new CustomEvent("hide-orientation"));
+ }
+
+ // If the page size is set, check whether we should use it as our paper size.
+ let isUsingPageRuleSizeAsPaperSize =
+ settings.usePageRuleSizeAsPaperSize &&
+ pageWidth !== null &&
+ pageHeight !== null;
+ if (isUsingPageRuleSizeAsPaperSize) {
+ // We canonically represent paper sizes using the width/height of a portrait-oriented sheet,
+ // with landscape-orientation applied as a supplemental rotation.
+ // If the page-size is landscape oriented, we flip the pageWidth / pageHeight here
+ // in order to pass a canonical representation into the paper-size settings.
+ if (orientation == "landscape") {
+ [pageHeight, pageWidth] = [pageWidth, pageHeight];
+ }
+
+ let matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
+ pageWidth,
+ pageHeight,
+ settings.kPaperSizeInches
+ );
+ if (matchedPaper) {
+ settings.paperId = matchedPaper.id;
+ }
+
+ settings.paperWidth = pageWidth;
+ settings.paperHeight = pageHeight;
+ settings.paperSizeUnit = settings.kPaperSizeInches;
+ document.dispatchEvent(new CustomEvent("hide-paper-size"));
+ }
+
+ this.previewIsEmpty = isEmpty;
+ // If the preview is empty, we know our range is greater than the number of pages.
+ // We have to send a pageRange update to display a non-empty page.
+ if (this.previewIsEmpty) {
+ this.viewSettings.pageRanges = [];
+ this.updatePrintPreview();
+ }
+
+ document.dispatchEvent(
+ new CustomEvent("page-count", {
+ detail: { sheetCount, totalPages: totalPageCount },
+ })
+ );
+
+ if (isFirstCall) {
+ const elapsed = Date.now() - this.printInitiationTime;
+ Services.telemetry
+ .getHistogramById("PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS")
+ .add(elapsed);
+ }
+ },
+
+ async getPrintDestinations() {
+ const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
+ Ci.nsIPrinterList
+ );
+ let printers;
+
+ if (Cu.isInAutomation) {
+ printers = window._mockPrinters || [];
+ } else {
+ try {
+ printers = await printerList.printers;
+ } catch (e) {
+ this.reportPrintingError("PRINTER_LIST");
+ throw e;
+ }
+ }
+
+ let fallbackPaperList;
+ try {
+ fallbackPaperList = await printerList.fallbackPaperList;
+ } catch (e) {
+ this.reportPrintingError("FALLBACK_PAPER_LIST");
+ throw e;
+ }
+
+ let lastUsedPrinterName;
+ try {
+ lastUsedPrinterName = PSSVC.lastUsedPrinterName;
+ } catch (e) {
+ this.reportPrintingError("LAST_USED_PRINTER");
+ throw e;
+ }
+ const defaultPrinterName = printerList.systemDefaultPrinterName;
+ const printersByName = {};
+
+ let lastUsedPrinter;
+ let defaultSystemPrinter;
+
+ let saveToPdfPrinter = {
+ nameId: "printui-destination-pdf-label",
+ value: PrintUtils.SAVE_TO_PDF_PRINTER,
+ };
+ printersByName[PrintUtils.SAVE_TO_PDF_PRINTER] = {
+ supportsColor: true,
+ supportsMonochrome: false,
+ name: PrintUtils.SAVE_TO_PDF_PRINTER,
+ };
+
+ if (lastUsedPrinterName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ lastUsedPrinter = saveToPdfPrinter;
+ }
+
+ let destinations = [
+ saveToPdfPrinter,
+ ...printers.map(printer => {
+ printer.QueryInterface(Ci.nsIPrinter);
+ const { name } = printer;
+ printersByName[printer.name] = { printer };
+ const destination = { name, value: name };
+
+ if (name == lastUsedPrinterName) {
+ lastUsedPrinter = destination;
+ }
+ if (name == defaultPrinterName) {
+ defaultSystemPrinter = destination;
+ }
+
+ return destination;
+ }),
+ ];
+
+ let selectedPrinter =
+ lastUsedPrinter || defaultSystemPrinter || saveToPdfPrinter;
+
+ return {
+ destinations,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName,
+ defaultSystemPrinter,
+ };
+ },
+
+ getMarginPresets(marginSize, paperWrapper) {
+ switch (marginSize) {
+ case "minimum": {
+ let marginSource = paperWrapper || this.defaultSettings;
+ return {
+ marginTop: marginSource.unwriteableMarginTop,
+ marginRight: marginSource.unwriteableMarginRight,
+ marginBottom: marginSource.unwriteableMarginBottom,
+ marginLeft: marginSource.unwriteableMarginLeft,
+ };
+ }
+ case "none":
+ return {
+ marginTop: 0,
+ marginLeft: 0,
+ marginBottom: 0,
+ marginRight: 0,
+ };
+ case "custom":
+ return {
+ marginTop:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginTop ??
+ this.settings.marginTop,
+ marginBottom:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginBottom ??
+ this.settings.marginBottom,
+ marginLeft:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginLeft ??
+ this.settings.marginLeft,
+ marginRight:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginRight ??
+ this.settings.marginRight,
+ };
+ default: {
+ let minimum = this.getMarginPresets("minimum", paperWrapper);
+ return {
+ marginTop: !isNaN(minimum.marginTop)
+ ? Math.max(minimum.marginTop, this.defaultSettings.marginTop)
+ : this.defaultSettings.marginTop,
+ marginRight: !isNaN(minimum.marginRight)
+ ? Math.max(minimum.marginRight, this.defaultSettings.marginRight)
+ : this.defaultSettings.marginRight,
+ marginBottom: !isNaN(minimum.marginBottom)
+ ? Math.max(minimum.marginBottom, this.defaultSettings.marginBottom)
+ : this.defaultSettings.marginBottom,
+ marginLeft: !isNaN(minimum.marginLeft)
+ ? Math.max(minimum.marginLeft, this.defaultSettings.marginLeft)
+ : this.defaultSettings.marginLeft,
+ };
+ }
+ }
+ },
+
+ reportPrintingError(aMessage) {
+ logger.debug("reportPrintingError:", aMessage);
+ Services.telemetry.keyedScalarAdd("printing.error", aMessage, 1);
+ },
+
+ /**
+ * Shows the system dialog. This method has been abstracted into a helper for
+ * testing purposes. The showPrintDialog() call blocks until the dialog is
+ * closed, so we mark it as async to allow us to reject from the test.
+ */
+ async _showPrintDialog(aWindow, aHaveSelection, aSettings) {
+ return PrintUtils.handleSystemPrintDialog(
+ aWindow,
+ aHaveSelection,
+ aSettings
+ );
+ },
+};
+
+var PrintSettingsViewProxy = {
+ get defaultHeadersAndFooterValues() {
+ const defaultBranch = Services.prefs.getDefaultBranch("");
+ let settingValues = {};
+ for (let [name, pref] of Object.entries(this.headerFooterSettingsPrefs)) {
+ settingValues[name] = defaultBranch.getStringPref(pref);
+ }
+ // We only need to retrieve these defaults once and they will not change
+ Object.defineProperty(this, "defaultHeadersAndFooterValues", {
+ value: settingValues,
+ });
+ return settingValues;
+ },
+
+ headerFooterSettingsPrefs: {
+ footerStrCenter: "print.print_footercenter",
+ footerStrLeft: "print.print_footerleft",
+ footerStrRight: "print.print_footerright",
+ headerStrCenter: "print.print_headercenter",
+ headerStrLeft: "print.print_headerleft",
+ headerStrRight: "print.print_headerright",
+ },
+
+ // Custom margins are not saved by a pref, so we need to keep track of them
+ // in order to save the value.
+ _lastCustomMarginValues: {
+ marginTop: null,
+ marginBottom: null,
+ marginLeft: null,
+ marginRight: null,
+ },
+
+ // This list was taken from nsDeviceContextSpecWin.cpp which records telemetry on print target type
+ knownSaveToFilePrinters: new Set([
+ "Microsoft Print to PDF",
+ "Adobe PDF",
+ "Bullzip PDF Printer",
+ "CutePDF Writer",
+ "doPDF",
+ "Foxit Reader PDF Printer",
+ "Nitro PDF Creator",
+ "novaPDF",
+ "PDF-XChange",
+ "PDF24 PDF",
+ "PDFCreator",
+ "PrimoPDF",
+ "Soda PDF",
+ "Solid PDF Creator",
+ "Universal Document Converter",
+ "Microsoft XPS Document Writer",
+ ]),
+
+ getBestPaperMatch(paperWidth, paperHeight, paperSizeUnit) {
+ let paperSizes = Object.values(this.availablePaperSizes);
+ if (!(paperWidth && paperHeight)) {
+ return null;
+ }
+ // first try to match on the paper dimensions using the current units
+ let unitsPerPoint;
+ let altUnitsPerPoint;
+ if (paperSizeUnit == PrintEventHandler.settings.kPaperSizeMillimeters) {
+ unitsPerPoint = MM_PER_POINT;
+ altUnitsPerPoint = INCHES_PER_POINT;
+ } else {
+ unitsPerPoint = INCHES_PER_POINT;
+ altUnitsPerPoint = MM_PER_POINT;
+ }
+ // equality to 1pt.
+ const equal = (a, b) => Math.abs(a - b) < 1;
+ const findMatch = (widthPts, heightPts) =>
+ paperSizes.find(paperWrapper => {
+ // the dimensions on the nsIPaper object are in points
+ let result =
+ equal(widthPts, paperWrapper.paper.width) &&
+ equal(heightPts, paperWrapper.paper.height);
+ return result;
+ });
+ // Look for a paper with matching dimensions, using the current printer's
+ // paper size unit, then the alternate unit
+ let matchedPaper =
+ findMatch(paperWidth / unitsPerPoint, paperHeight / unitsPerPoint) ||
+ findMatch(paperWidth / altUnitsPerPoint, paperHeight / altUnitsPerPoint);
+
+ if (matchedPaper) {
+ return matchedPaper;
+ }
+ return null;
+ },
+
+ async fetchPaperMargins(paperId) {
+ // resolve any async and computed properties we need on the paper
+ let paperWrapper = this.availablePaperSizes[paperId];
+ if (!paperWrapper) {
+ throw new Error("Can't fetchPaperMargins: " + paperId);
+ }
+ if (paperWrapper._resolved) {
+ // We've already resolved and calculated these values
+ return paperWrapper;
+ }
+ let margins;
+ try {
+ margins = await paperWrapper.paper.unwriteableMargin;
+ } catch (e) {
+ this.reportPrintingError("UNWRITEABLE_MARGIN");
+ throw e;
+ }
+ margins.QueryInterface(Ci.nsIPaperMargin);
+
+ // margin dimensions are given on the paper in points, setting values need to be in inches
+ paperWrapper.unwriteableMarginTop = margins.top * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginRight = margins.right * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginBottom = margins.bottom * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginLeft = margins.left * INCHES_PER_POINT;
+ // No need to re-resolve static properties
+ paperWrapper._resolved = true;
+ return paperWrapper;
+ },
+
+ async resolvePropertiesForPrinter(printerName) {
+ // resolve any async properties we need on the printer
+ let printerInfo = this.availablePrinters[printerName];
+ if (printerInfo._resolved) {
+ // Store a convenience reference
+ this.availablePaperSizes = printerInfo.availablePaperSizes;
+ return printerInfo;
+ }
+
+ // Await the async printer data.
+ if (printerInfo.printer) {
+ let basePrinterInfo;
+ try {
+ [
+ printerInfo.supportsDuplex,
+ printerInfo.supportsColor,
+ printerInfo.supportsMonochrome,
+ basePrinterInfo,
+ ] = await Promise.all([
+ printerInfo.printer.supportsDuplex,
+ printerInfo.printer.supportsColor,
+ printerInfo.printer.supportsMonochrome,
+ printerInfo.printer.printerInfo,
+ ]);
+ } catch (e) {
+ this.reportPrintingError("PRINTER_SETTINGS");
+ throw e;
+ }
+ basePrinterInfo.QueryInterface(Ci.nsIPrinterInfo);
+ basePrinterInfo.defaultSettings.QueryInterface(Ci.nsIPrintSettings);
+
+ printerInfo.paperList = basePrinterInfo.paperList;
+ printerInfo.defaultSettings = basePrinterInfo.defaultSettings;
+ } else if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ // The Mozilla PDF pseudo-printer has no actual nsIPrinter implementation
+ printerInfo.defaultSettings = PSSVC.createNewPrintSettings();
+ printerInfo.defaultSettings.printerName = printerName;
+ printerInfo.defaultSettings.toFileName = "";
+ printerInfo.defaultSettings.outputFormat =
+ Ci.nsIPrintSettings.kOutputFormatPDF;
+ printerInfo.defaultSettings.outputDestination =
+ Ci.nsIPrintSettings.kOutputDestinationFile;
+ printerInfo.defaultSettings.usePageRuleSizeAsPaperSize =
+ Services.prefs.getBoolPref(
+ "print.save_as_pdf.use_page_rule_size_as_paper_size.enabled",
+ false
+ );
+ printerInfo.paperList = this.fallbackPaperList;
+ }
+ printerInfo.settings = printerInfo.defaultSettings.clone();
+ // Apply any previously persisted user values
+ // Don't apply kInitSavePrintToFile though, that should only be true for
+ // the PDF printer.
+ printerInfo.settings.outputDestination =
+ printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? Ci.nsIPrintSettings.kOutputDestinationFile
+ : Ci.nsIPrintSettings.kOutputDestinationPrinter;
+ let flags =
+ printerInfo.settings.kInitSaveAll ^
+ printerInfo.settings.kInitSavePrintToFile;
+ PSSVC.initPrintSettingsFromPrefs(printerInfo.settings, true, flags);
+ // We set `isInitializedFromPrinter` to make sure that that's set on the
+ // SAVE_TO_PDF_PRINTER settings. The naming is poor, but that tells the
+ // platform code that the settings object is complete.
+ printerInfo.settings.isInitializedFromPrinter = true;
+
+ printerInfo.settings.toFileName = "";
+
+ // prepare the available paper sizes for this printer
+ if (!printerInfo.paperList?.length) {
+ logger.warn(
+ "Printer has empty paperList: ",
+ printerInfo.printer.id,
+ "using fallbackPaperList"
+ );
+ printerInfo.paperList = this.fallbackPaperList;
+ }
+ // don't trust the settings to provide valid paperSizeUnit values
+ let sizeUnit =
+ printerInfo.settings.paperSizeUnit ==
+ printerInfo.settings.kPaperSizeMillimeters
+ ? printerInfo.settings.kPaperSizeMillimeters
+ : printerInfo.settings.kPaperSizeInches;
+ let papersById = (printerInfo.availablePaperSizes = {});
+ // Store a convenience reference
+ this.availablePaperSizes = papersById;
+
+ for (let paper of printerInfo.paperList) {
+ paper.QueryInterface(Ci.nsIPaper);
+ // Bug 1662239: I'm seeing multiple duplicate entries for each paper size
+ // so ensure we have one entry per name
+ if (!papersById[paper.id]) {
+ papersById[paper.id] = {
+ paper,
+ id: paper.id,
+ name: paper.name,
+ // XXXsfoster: Eventually we want to get the unit from the nsIPaper object
+ sizeUnit,
+ };
+ }
+ }
+ // Update our cache of all the paper sizes by name
+ Object.assign(PrintEventHandler.allPaperSizes, papersById);
+
+ // The printer properties don't change, mark this as resolved for next time
+ printerInfo._resolved = true;
+ logger.debug("Resolved printerInfo:", printerInfo);
+ return printerInfo;
+ },
+
+ get(target, name) {
+ switch (name) {
+ case "currentPaper": {
+ let paperId = this.get(target, "paperId");
+ return paperId && this.availablePaperSizes[paperId];
+ }
+
+ case "marginPresets":
+ let paperWrapper = this.get(target, "currentPaper");
+ return {
+ none: PrintEventHandler.getMarginPresets("none", paperWrapper),
+ minimum: PrintEventHandler.getMarginPresets("minimum", paperWrapper),
+ default: PrintEventHandler.getMarginPresets("default", paperWrapper),
+ custom: PrintEventHandler.getMarginPresets("custom", paperWrapper),
+ };
+
+ case "marginOptions": {
+ let allMarginPresets = this.get(target, "marginPresets");
+ let uniqueMargins = new Set();
+ let marginsEnabled = {};
+ for (let name of ["none", "default", "minimum", "custom"]) {
+ let { marginTop, marginLeft, marginBottom, marginRight } =
+ allMarginPresets[name];
+ let key = [marginTop, marginLeft, marginBottom, marginRight].join(
+ ","
+ );
+ // Custom margins are initialized to default margins
+ marginsEnabled[name] = !uniqueMargins.has(key) || name == "custom";
+ uniqueMargins.add(key);
+ }
+ return marginsEnabled;
+ }
+
+ case "margins":
+ let marginSettings = {
+ marginTop: target.marginTop,
+ marginLeft: target.marginLeft,
+ marginBottom: target.marginBottom,
+ marginRight: target.marginRight,
+ };
+ // see if they match the none, minimum, or default margin values
+ let allMarginPresets = this.get(target, "marginPresets");
+ const marginsMatch = function (lhs, rhs) {
+ return Object.keys(marginSettings).every(
+ name => lhs[name].toFixed(2) == rhs[name].toFixed(2)
+ );
+ };
+ const potentialPresets = (function () {
+ let presets = [];
+ const minimumIsNone = marginsMatch(
+ allMarginPresets.none,
+ allMarginPresets.minimum
+ );
+ // We only attempt to match the serialized values against the "none"
+ // preset if the unwriteable margins are being ignored or are zero.
+ if (target.ignoreUnwriteableMargins || minimumIsNone) {
+ presets.push("none");
+ }
+ if (!minimumIsNone) {
+ presets.push("minimum");
+ }
+ presets.push("default");
+ return presets;
+ })();
+ for (let presetName of potentialPresets) {
+ let marginPresets = allMarginPresets[presetName];
+ if (marginsMatch(marginSettings, marginPresets)) {
+ return presetName;
+ }
+ }
+
+ // Fall back to custom for other values
+ return "custom";
+
+ case "defaultMargins":
+ return PrintEventHandler.getMarginPresets(
+ "default",
+ this.get(target, "currentPaper")
+ );
+
+ case "customMargins":
+ return PrintEventHandler.getMarginPresets(
+ "custom",
+ this.get(target, "currentPaper")
+ );
+
+ case "paperSizes":
+ return Object.values(this.availablePaperSizes)
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map(paper => {
+ return {
+ name: paper.name,
+ value: paper.id,
+ };
+ });
+
+ case "supportsDuplex":
+ return this.availablePrinters[target.printerName].supportsDuplex;
+
+ case "printDuplex":
+ switch (target.duplex) {
+ case Ci.nsIPrintSettings.kDuplexNone:
+ break;
+ case Ci.nsIPrintSettings.kDuplexFlipOnLongEdge:
+ return "long-edge";
+ case Ci.nsIPrintSettings.kDuplexFlipOnShortEdge:
+ return "short-edge";
+ default:
+ logger.warn("Unexpected duplex value: ", target.duplex);
+ }
+ return "off";
+ case "printBackgrounds":
+ return target.printBGImages || target.printBGColors;
+
+ case "printFootersHeaders":
+ // if any of the footer and headers settings have a non-empty string value
+ // we consider that "enabled"
+ return Object.keys(this.headerFooterSettingsPrefs).some(
+ name => !!target[name]
+ );
+
+ case "supportsColor":
+ return this.availablePrinters[target.printerName].supportsColor;
+
+ case "willSaveToFile":
+ return (
+ target.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF ||
+ this.knownSaveToFilePrinters.has(target.printerName)
+ );
+ case "supportsMonochrome":
+ return this.availablePrinters[target.printerName].supportsMonochrome;
+ case "defaultSystemPrinter":
+ return (
+ this.defaultSystemPrinter?.value ||
+ Object.getOwnPropertyNames(this.availablePrinters).find(
+ name => name != PrintUtils.SAVE_TO_PDF_PRINTER
+ )
+ );
+
+ case "numCopies":
+ return this.get(target, "willSaveToFile") ? 1 : target.numCopies;
+ case "sourceVersion":
+ return this._sourceVersion;
+ }
+ return target[name];
+ },
+
+ set(target, name, value) {
+ switch (name) {
+ case "margins":
+ if (!["default", "minimum", "none", "custom"].includes(value)) {
+ logger.warn("Unexpected margin preset name: ", value);
+ value = "default";
+ }
+ let paperWrapper = this.get(target, "currentPaper");
+ let marginPresets = PrintEventHandler.getMarginPresets(
+ value,
+ paperWrapper
+ );
+ for (let [settingName, presetValue] of Object.entries(marginPresets)) {
+ target[settingName] = presetValue;
+ }
+ target.honorPageRuleMargins = value == "default";
+ target.ignoreUnwriteableMargins = value == "none";
+ break;
+
+ case "paperId": {
+ let paperId = value;
+ let paperWrapper = this.availablePaperSizes[paperId];
+ // Dimensions on the paper object are in pts.
+ // We convert to the printer's specified unit when updating settings
+ let unitsPerPoint =
+ paperWrapper.sizeUnit == target.kPaperSizeMillimeters
+ ? MM_PER_POINT
+ : INCHES_PER_POINT;
+ // paperWidth and paperHeight are calculated values that we always treat as suspect and
+ // re-calculate whenever the paperId changes
+ target.paperSizeUnit = paperWrapper.sizeUnit;
+ target.paperWidth = paperWrapper.paper.width * unitsPerPoint;
+ target.paperHeight = paperWrapper.paper.height * unitsPerPoint;
+ // Unwriteable margins were pre-calculated from their async values when the paper size
+ // was selected. They are always in inches
+ target.unwriteableMarginTop = paperWrapper.unwriteableMarginTop;
+ target.unwriteableMarginRight = paperWrapper.unwriteableMarginRight;
+ target.unwriteableMarginBottom = paperWrapper.unwriteableMarginBottom;
+ target.unwriteableMarginLeft = paperWrapper.unwriteableMarginLeft;
+ target.paperId = paperWrapper.paper.id;
+ // pull new margin values for the new paper size
+ this.set(target, "margins", this.get(target, "margins"));
+ break;
+ }
+
+ case "printerName":
+ // Can't set printerName, settings objects belong to a specific printer.
+ break;
+
+ case "printBackgrounds":
+ target.printBGImages = value;
+ target.printBGColors = value;
+ break;
+
+ case "printDuplex": {
+ let duplex = (function () {
+ switch (value) {
+ case "off":
+ break;
+ case "long-edge":
+ return Ci.nsIPrintSettings.kDuplexFlipOnLongEdge;
+ case "short-edge":
+ return Ci.nsIPrintSettings.kDuplexFlipOnShortEdge;
+ default:
+ logger.warn("Unexpected duplex name: ", value);
+ }
+ return Ci.nsIPrintSettings.kDuplexNone;
+ })();
+
+ target.duplex = duplex;
+ break;
+ }
+
+ case "printFootersHeaders":
+ // To disable header & footers, set them all to empty.
+ // To enable, restore default values for each of the header & footer settings.
+ for (let [settingName, defaultValue] of Object.entries(
+ this.defaultHeadersAndFooterValues
+ )) {
+ target[settingName] = value ? defaultValue : "";
+ }
+ break;
+
+ case "customMargins":
+ if (value != null) {
+ for (let [settingName, newVal] of Object.entries(value)) {
+ target[settingName] = newVal;
+ this._lastCustomMarginValues[settingName] = newVal;
+ }
+ }
+ break;
+
+ case "customMarginTop":
+ case "customMarginBottom":
+ case "customMarginLeft":
+ case "customMarginRight":
+ let customMarginName = "margin" + name.substring(12);
+ this.set(
+ target,
+ "customMargins",
+ Object.assign({}, this.get(target, "customMargins"), {
+ [customMarginName]: value,
+ })
+ );
+ break;
+
+ case "sourceVersion":
+ this._sourceVersion = value;
+ this.set(target, "printSelectionOnly", value == "selection");
+ if (value == "simplified") {
+ this.set(target, "printBackgrounds", false);
+ }
+ break;
+
+ default:
+ target[name] = value;
+ }
+ },
+};
+
+/*
+ * Custom elements ----------------------------------------------------
+ */
+
+function PrintUIControlMixin(superClass) {
+ return class PrintUIControl extends superClass {
+ connectedCallback() {
+ this.setAttribute("autocomplete", "off");
+ this.initialize();
+ this.render();
+ }
+
+ initialize() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ if (this.templateId) {
+ let template = this.ownerDocument.getElementById(this.templateId);
+ let templateContent = template.content;
+ this.appendChild(templateContent.cloneNode(true));
+ }
+
+ document.addEventListener("print-settings", ({ detail: settings }) => {
+ this.update(settings);
+ });
+
+ this.addEventListener("input", this);
+ }
+
+ render() {}
+
+ update(settings) {}
+
+ dispatchSettingsChange(changedSettings) {
+ this.dispatchEvent(
+ new CustomEvent("update-print-settings", {
+ bubbles: true,
+ detail: changedSettings,
+ })
+ );
+ }
+
+ cancelSettingsChange(changedSettings) {
+ this.dispatchEvent(
+ new CustomEvent("cancel-print-settings", {
+ bubbles: true,
+ detail: changedSettings,
+ })
+ );
+ }
+
+ handleEvent(event) {}
+ };
+}
+
+class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) {
+ initialize() {
+ super.initialize();
+
+ this.addEventListener("submit", this);
+ this.addEventListener("click", this);
+ this.addEventListener("revalidate", this);
+
+ this._printerDestination = this.querySelector("#destination");
+ this.printButton = this.querySelector("#print-button");
+ }
+
+ removeNonPdfSettings() {
+ let selectors = ["#backgrounds", "#source-version-selection"];
+ for (let selector of selectors) {
+ this.querySelector(selector).remove();
+ }
+ let moreSettings = this.querySelector("#more-settings-options");
+ if (moreSettings.children.length <= 1) {
+ moreSettings.remove();
+ }
+ }
+
+ requestPrint() {
+ this.requestSubmit(this.printButton);
+ }
+
+ update(settings) {
+ // If there are no default system printers available and we are not on mac,
+ // we should hide the system dialog because it won't be populated with
+ // the correct settings. Mac and Gtk support save to pdf functionality
+ // in the native dialog, so it can be shown regardless.
+ this.querySelector("#system-print").hidden =
+ AppConstants.platform === "win" && !settings.defaultSystemPrinter;
+
+ this.querySelector("#two-sided-printing").hidden = !settings.supportsDuplex;
+ }
+
+ enable() {
+ let isValid = this.checkValidity();
+ document.body.toggleAttribute("invalid", !isValid);
+ if (isValid) {
+ for (let element of this.elements) {
+ if (!element.hasAttribute("disallowed")) {
+ element.disabled = false;
+ }
+ }
+ // aria-describedby will usually cause the first value to be reported.
+ // Unfortunately, screen readers don't pick up description changes from
+ // dialogs, so we must use a live region. To avoid double reporting of
+ // the first value, we don't set aria-live initially. We only set it for
+ // subsequent updates.
+ // aria-live is set on the parent because sheetCount itself might be
+ // hidden and then shown, and updates are only reported for live
+ // regions that were already visible.
+ document
+ .querySelector("#sheet-count")
+ .parentNode.setAttribute("aria-live", "polite");
+ } else {
+ // Find the invalid element
+ let invalidElement;
+ for (let element of this.elements) {
+ if (!element.checkValidity()) {
+ invalidElement = element;
+ break;
+ }
+ }
+ let section = invalidElement.closest(".section-block");
+ document.body.toggleAttribute("invalid", !isValid);
+ // We're hiding the sheet count and aria-describedby includes the
+ // content of hidden elements, so remove aria-describedby.
+ document.body.removeAttribute("aria-describedby");
+ for (let element of this.elements) {
+ // If we're valid, enable all inputs.
+ // Otherwise, disable the valid inputs other than the cancel button and the elements
+ // in the invalid section.
+ element.disabled =
+ element.hasAttribute("disallowed") ||
+ (!isValid &&
+ element.validity.valid &&
+ element.name != "cancel" &&
+ element.closest(".section-block") != this._printerDestination &&
+ element.closest(".section-block") != section);
+ }
+ }
+ }
+
+ disable(filterFn) {
+ for (let element of this.elements) {
+ if (filterFn && !filterFn(element)) {
+ continue;
+ }
+ element.disabled = element.name != "cancel";
+ }
+ }
+
+ handleEvent(e) {
+ if (e.target.id == "open-dialog-link") {
+ this.dispatchEvent(new Event("open-system-dialog", { bubbles: true }));
+ return;
+ }
+
+ if (e.type == "submit") {
+ e.preventDefault();
+ if (e.submitter.name == "print" && this.checkValidity()) {
+ this.dispatchEvent(new Event("print", { bubbles: true }));
+ }
+ } else if (
+ (e.type == "input" || e.type == "revalidate") &&
+ !this.printerChanging
+ ) {
+ this.enable();
+ }
+ }
+}
+customElements.define("print-form", PrintUIForm, { extends: "form" });
+
+class PrintSettingSelect extends PrintUIControlMixin(HTMLSelectElement) {
+ initialize() {
+ super.initialize();
+ this.addEventListener("keypress", this);
+ }
+
+ connectedCallback() {
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ setOptions(optionValues = []) {
+ this.textContent = "";
+ for (let optionData of optionValues) {
+ let opt = new Option(
+ optionData.name,
+ "value" in optionData ? optionData.value : optionData.name
+ );
+ if (optionData.nameId) {
+ document.l10n.setAttributes(opt, optionData.nameId);
+ }
+ // option selectedness is set via update() and assignment to this.value
+ this.options.add(opt);
+ }
+ }
+
+ update(settings) {
+ if (this.settingName) {
+ this.value = settings[this.settingName];
+ }
+ }
+
+ handleEvent(e) {
+ if (e.type == "input" && this.settingName) {
+ this.dispatchSettingsChange({
+ [this.settingName]: e.target.value,
+ });
+ } else if (e.type == "keypress") {
+ if (
+ e.key == "Enter" &&
+ (!e.metaKey || AppConstants.platform == "macosx")
+ ) {
+ this.form.requestPrint();
+ }
+ }
+ }
+}
+customElements.define("setting-select", PrintSettingSelect, {
+ extends: "select",
+});
+
+class PrintSettingNumber extends PrintUIControlMixin(HTMLInputElement) {
+ initialize() {
+ super.initialize();
+ this.addEventListener("beforeinput", e => this.preventWhitespaceEntry(e));
+ this.addEventListener("paste", e => this.pasteWithoutWhitespace(e));
+ }
+
+ connectedCallback() {
+ this.type = "number";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ if (this.settingName) {
+ this.value = settings[this.settingName];
+ }
+ }
+
+ preventWhitespaceEntry(e) {
+ if (e.data && !e.data.trim().length) {
+ e.preventDefault();
+ }
+ }
+
+ pasteWithoutWhitespace(e) {
+ // Prevent original value from being pasted
+ e.preventDefault();
+
+ // Manually update input's value with sanitized clipboard data
+ let paste = (e.clipboardData || window.clipboardData)
+ .getData("text")
+ .trim();
+ this.value = paste;
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "input":
+ if (this.settingName && this.checkValidity()) {
+ this.dispatchSettingsChange({
+ [this.settingName]: this.value,
+ });
+ }
+ break;
+ }
+ }
+}
+customElements.define("setting-number", PrintSettingNumber, {
+ extends: "input",
+});
+
+class PrintSettingCheckbox extends PrintUIControlMixin(HTMLInputElement) {
+ connectedCallback() {
+ this.type = "checkbox";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ this.checked = settings[this.settingName];
+ }
+
+ handleEvent(e) {
+ this.dispatchSettingsChange({
+ [this.settingName]: this.checked,
+ });
+ }
+}
+customElements.define("setting-checkbox", PrintSettingCheckbox, {
+ extends: "input",
+});
+
+class PrintSettingRadio extends PrintUIControlMixin(HTMLInputElement) {
+ connectedCallback() {
+ this.type = "radio";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ this.checked = settings[this.settingName] == this.value;
+ }
+
+ handleEvent(e) {
+ this.dispatchSettingsChange({
+ [this.settingName]: this.value,
+ });
+ }
+}
+customElements.define("setting-radio", PrintSettingRadio, {
+ extends: "input",
+});
+
+class DestinationPicker extends PrintSettingSelect {
+ initialize() {
+ super.initialize();
+ document.addEventListener("available-destinations", this);
+ }
+
+ update(settings) {
+ super.update(settings);
+ let isPdf = settings.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF;
+ this.setAttribute("output", isPdf ? "pdf" : "paper");
+ }
+
+ handleEvent(e) {
+ super.handleEvent(e);
+
+ if (e.type == "available-destinations") {
+ this.setOptions(e.detail);
+ }
+ }
+}
+customElements.define("destination-picker", DestinationPicker, {
+ extends: "select",
+});
+
+class ColorModePicker extends PrintSettingSelect {
+ update(settings) {
+ this.value = settings[this.settingName] ? "color" : "bw";
+ let canSwitch = settings.supportsColor && settings.supportsMonochrome;
+ if (this.disablePicker != canSwitch) {
+ this.toggleAttribute("disallowed", !canSwitch);
+ this.disabled = !canSwitch;
+ }
+ this.disablePicker = canSwitch;
+ }
+
+ handleEvent(e) {
+ if (e.type == "input") {
+ // turn our string value into the expected boolean
+ this.dispatchSettingsChange({
+ [this.settingName]: this.value == "color",
+ });
+ }
+ }
+}
+customElements.define("color-mode-select", ColorModePicker, {
+ extends: "select",
+});
+
+class PaperSizePicker extends PrintSettingSelect {
+ initialize() {
+ super.initialize();
+ this._printerName = null;
+ this._section = this.closest(".section-block");
+ document.addEventListener("hide-paper-size", this);
+ }
+
+ update(settings) {
+ if (settings.printerName !== this._printerName) {
+ this._printerName = settings.printerName;
+ this.setOptions(settings.paperSizes);
+ }
+ this.value = settings.paperId;
+
+ // Unhide the paper-size picker, if we've stopped using the page size as paper-size.
+ if (this._section.hidden && !settings.usePageRuleSizeAsPaperSize) {
+ this._section.hidden = false;
+ }
+ }
+
+ handleEvent(e) {
+ super.handleEvent(e);
+ const { type } = e;
+ if (type == "hide-paper-size") {
+ this._section.hidden = true;
+ }
+ }
+}
+customElements.define("paper-size-select", PaperSizePicker, {
+ extends: "select",
+});
+
+class OrientationInput extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+ document.addEventListener("hide-orientation", this);
+ }
+
+ get templateId() {
+ return "orientation-template";
+ }
+
+ update(settings) {
+ for (let input of this.querySelectorAll("input")) {
+ input.checked = settings.orientation == input.value;
+ }
+ }
+
+ handleEvent(e) {
+ if (e.type == "hide-orientation") {
+ document.getElementById("orientation").hidden = true;
+ return;
+ }
+ this.dispatchSettingsChange({
+ orientation: e.target.value,
+ });
+ }
+}
+customElements.define("orientation-input", OrientationInput);
+
+class CopiesInput extends PrintUIControlMixin(HTMLElement) {
+ get templateId() {
+ return "copy-template";
+ }
+
+ initialize() {
+ super.initialize();
+ this._copiesSection = this.closest(".section-block");
+ this._copiesInput = this.querySelector("#copies-count");
+ this._copiesError = this.querySelector("#error-invalid-copies");
+ }
+
+ update(settings) {
+ this._copiesSection.hidden = settings.willSaveToFile;
+ this._copiesError.hidden = true;
+ }
+
+ handleEvent(e) {
+ this._copiesError.hidden = this._copiesInput.checkValidity();
+ }
+}
+customElements.define("copy-count-input", CopiesInput);
+
+class ScaleInput extends PrintUIControlMixin(HTMLElement) {
+ get templateId() {
+ return "scale-template";
+ }
+
+ initialize() {
+ super.initialize();
+
+ this._percentScale = this.querySelector("#percent-scale");
+ this._shrinkToFitChoice = this.querySelector("#fit-choice");
+ this._scaleChoice = this.querySelector("#percent-scale-choice");
+ this._scaleError = this.querySelector("#error-invalid-scale");
+ }
+
+ updateScale() {
+ this.dispatchSettingsChange({
+ scaling: Number(this._percentScale.value / 100),
+ });
+ }
+
+ update(settings) {
+ let { scaling, shrinkToFit, printerName } = settings;
+ this._shrinkToFitChoice.checked = shrinkToFit;
+ this._scaleChoice.checked = !shrinkToFit;
+ if (this.disableScale != shrinkToFit) {
+ this._percentScale.disabled = shrinkToFit;
+ this._percentScale.toggleAttribute("disallowed", shrinkToFit);
+ }
+ this.disableScale = shrinkToFit;
+ if (!this.printerName) {
+ this.printerName = printerName;
+ }
+
+ // If the user had an invalid input and switches back to "fit to page",
+ // we repopulate the scale field with the stored, valid scaling value.
+ let isValid = this._percentScale.checkValidity();
+ if (
+ !this._percentScale.value ||
+ (this._shrinkToFitChoice.checked && !isValid) ||
+ (this.printerName != printerName && !isValid)
+ ) {
+ // Only allow whole numbers. 0.14 * 100 would have decimal places, etc.
+ this._percentScale.value = parseInt(scaling * 100, 10);
+ this.printerName = printerName;
+ if (!isValid) {
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._scaleError.hidden = true;
+ }
+ }
+ }
+
+ handleEvent(e) {
+ if (e.target == this._shrinkToFitChoice || e.target == this._scaleChoice) {
+ if (!this._percentScale.checkValidity()) {
+ this._percentScale.value = 100;
+ }
+ let scale =
+ e.target == this._shrinkToFitChoice
+ ? 1
+ : Number(this._percentScale.value / 100);
+ this.dispatchSettingsChange({
+ shrinkToFit: this._shrinkToFitChoice.checked,
+ scaling: scale,
+ });
+ this._scaleError.hidden = true;
+ } else if (e.type == "input") {
+ if (this._percentScale.checkValidity()) {
+ this.updateScale();
+ }
+ }
+
+ window.clearTimeout(this.showErrorTimeoutId);
+ if (this._percentScale.validity.valid) {
+ this._scaleError.hidden = true;
+ } else {
+ this.cancelSettingsChange({ scaling: true });
+ this.showErrorTimeoutId = window.setTimeout(() => {
+ this._scaleError.hidden = false;
+ }, INPUT_DELAY_MS);
+ }
+ }
+}
+customElements.define("scale-input", ScaleInput);
+
+class PageRangeInput extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+
+ this._rangeInput = this.querySelector("#custom-range");
+ this._rangeInput.title = "";
+ this._rangePicker = this.querySelector("#range-picker");
+ this._rangePickerEvenOption = this._rangePicker.namedItem("even");
+ this._rangeError = this.querySelector("#error-invalid-range");
+ this._startRangeOverflowError = this.querySelector(
+ "#error-invalid-start-range-overflow"
+ );
+
+ this._pagesSet = new Set();
+
+ this.addEventListener("keypress", this);
+ this.addEventListener("paste", this);
+ document.addEventListener("page-count", this);
+ }
+
+ get templateId() {
+ return "page-range-template";
+ }
+
+ updatePageRange() {
+ let isCustom = this._rangePicker.value == "custom";
+ let isCurrent = this._rangePicker.value == "current";
+
+ if (!isCurrent) {
+ this._currentPage = null;
+ }
+
+ if (isCustom) {
+ this.validateRangeInput();
+ } else if (isCurrent) {
+ this._currentPage = this._rangeInput.value =
+ this._currentPage || this.getCurrentVisiblePageNumber();
+ this.validateRangeInput();
+ } else {
+ this._pagesSet.clear();
+
+ if (this._rangePicker.value == "odd") {
+ for (let i = 1; i <= this._numPages; i += 2) {
+ this._pagesSet.add(i);
+ }
+ } else if (this._rangePicker.value == "even") {
+ for (let i = 2; i <= this._numPages; i += 2) {
+ this._pagesSet.add(i);
+ }
+ }
+
+ if (!this._rangeInput.checkValidity()) {
+ this._rangeInput.setCustomValidity("");
+ this._rangeInput.value = "";
+ }
+ }
+
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+
+ document.l10n.setAttributes(
+ this._rangeError,
+ "printui-error-invalid-range",
+ {
+ numPages: this._numPages,
+ }
+ );
+
+ // If it's valid, update the page range and hide the error messages.
+ // Otherwise, set the appropriate error message
+ if (this._rangeInput.validity.valid || !isCustom) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ this._startRangeOverflowError.hidden = this._rangeError.hidden = true;
+ } else {
+ this._rangeInput.focus();
+ }
+ }
+
+ dispatchPageRange(shouldCancel = true) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ if (
+ this._rangeInput.validity.valid ||
+ this._rangePicker.value != "custom"
+ ) {
+ this.dispatchSettingsChange({
+ pageRanges: this.formatPageRange(),
+ });
+ } else {
+ if (shouldCancel) {
+ this.cancelSettingsChange({ pageRanges: true });
+ }
+ this.showErrorTimeoutId = window.setTimeout(() => {
+ this._rangeError.hidden =
+ this._rangeInput.validationMessage != "invalid";
+ this._startRangeOverflowError.hidden =
+ this._rangeInput.validationMessage != "startRangeOverflow";
+ }, INPUT_DELAY_MS);
+ }
+ }
+
+ // The platform expects pageRanges to be an array of
+ // ranges represented by ints.
+ // Ex: Printing pages 1-3 would return [1,3]
+ // Ex: Printing page 1 would return [1,1]
+ // Ex: Printing pages 1-2,4 would return [1,2,4,4]
+ formatPageRange() {
+ if (
+ this._pagesSet.size == 0 ||
+ (this._rangePicker.value == "custom" && this._rangeInput.value == "") ||
+ this._rangePicker.value == "all"
+ ) {
+ // Show all pages.
+ return [];
+ }
+ let pages = Array.from(this._pagesSet).sort((a, b) => a - b);
+
+ let formattedRanges = [];
+ let startRange = pages[0];
+ let endRange = pages[0];
+ formattedRanges.push(startRange);
+
+ for (let i = 1; i < pages.length; i++) {
+ let currentPage = pages[i - 1];
+ let nextPage = pages[i];
+ if (nextPage > currentPage + 1) {
+ formattedRanges.push(endRange);
+ startRange = endRange = nextPage;
+ formattedRanges.push(startRange);
+ } else {
+ endRange = nextPage;
+ }
+ }
+ formattedRanges.push(endRange);
+
+ return formattedRanges;
+ }
+
+ update(settings) {
+ let { pageRanges, printerName } = settings;
+
+ this.toggleAttribute("all-pages", !pageRanges.length);
+ if (!this.printerName) {
+ this.printerName = printerName;
+ }
+
+ let isValid = this._rangeInput.checkValidity();
+
+ if (this.printerName != printerName && !isValid) {
+ this.printerName = printerName;
+ this._rangeInput.value = "";
+ this.updatePageRange();
+ this.dispatchPageRange();
+ }
+ }
+
+ handleKeypress(e) {
+ let char = String.fromCharCode(e.charCode);
+ let acceptedChar = char.match(/^[0-9,-]$/);
+ if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ }
+ }
+
+ handlePaste(e) {
+ let paste = (e.clipboardData || window.clipboardData)
+ .getData("text")
+ .trim();
+ if (!paste.match(/^[0-9,-]*$/)) {
+ e.preventDefault();
+ }
+ }
+
+ // This method has been abstracted into a helper for testing purposes
+ _validateRangeInput(value, numPages) {
+ this._pagesSet.clear();
+ var ranges = value.split(",");
+
+ for (let range of ranges) {
+ let rangeParts = range.split("-");
+ if (rangeParts.length > 2) {
+ this._rangeInput.setCustomValidity("invalid");
+ this._rangeInput.title = "";
+ this._pagesSet.clear();
+ return;
+ }
+ let startRange = parseInt(rangeParts[0], 10);
+ let endRange = parseInt(
+ rangeParts.length == 2 ? rangeParts[1] : rangeParts[0],
+ 10
+ );
+
+ if (isNaN(startRange) && isNaN(endRange)) {
+ continue;
+ }
+
+ // If the startRange was not specified, then we infer this
+ // to be 1.
+ if (isNaN(startRange) && rangeParts[0] == "") {
+ startRange = 1;
+ }
+ // If the end range was not specified, then we infer this
+ // to be the total number of pages.
+ if (isNaN(endRange) && rangeParts[1] == "") {
+ endRange = numPages;
+ }
+
+ // Check the range for errors
+ if (endRange < startRange) {
+ this._rangeInput.setCustomValidity("startRangeOverflow");
+ this._pagesSet.clear();
+ return;
+ } else if (
+ startRange > numPages ||
+ endRange > numPages ||
+ startRange == 0
+ ) {
+ this._rangeInput.setCustomValidity("invalid");
+ this._rangeInput.title = "";
+ this._pagesSet.clear();
+ return;
+ }
+
+ for (let i = startRange; i <= endRange; i++) {
+ this._pagesSet.add(i);
+ }
+ }
+
+ this._rangeInput.setCustomValidity("");
+ }
+
+ validateRangeInput() {
+ let value = ["custom", "current"].includes(this._rangePicker.value)
+ ? this._rangeInput.value
+ : "";
+ this._validateRangeInput(value, this._numPages);
+ }
+
+ getCurrentVisiblePageNumber() {
+ let pageNum = parseInt(
+ PrintEventHandler.printPreviewEl.lastPreviewBrowser.getAttribute(
+ "current-page"
+ )
+ );
+ return isNaN(pageNum) ? 1 : pageNum;
+ }
+
+ handleEvent(e) {
+ if (e.type == "keypress") {
+ if (e.target == this._rangeInput) {
+ this.handleKeypress(e);
+ }
+ return;
+ }
+
+ if (e.type === "paste" && e.target == this._rangeInput) {
+ this.handlePaste(e);
+ return;
+ }
+
+ if (e.type == "page-count") {
+ let { totalPages } = e.detail;
+ // This means we have already handled the page count event
+ // and do not need to dispatch another event.
+ if (this._numPages == totalPages) {
+ return;
+ }
+
+ this._numPages = totalPages;
+ this._rangeInput.disabled = false;
+ this._rangePickerEvenOption.disabled = this._numPages < 2;
+
+ let prevPages = Array.from(this._pagesSet);
+ this.updatePageRange();
+ if (
+ prevPages.length != this._pagesSet.size ||
+ !prevPages.every(page => this._pagesSet.has(page))
+ ) {
+ // If the calculated set of pages has changed then we need to dispatch
+ // a new pageRanges setting :(
+ // Ideally this would be resolved in the settings code since it should
+ // only happen for the "N-" case where pages N through the end of the
+ // document are in the range.
+ this.dispatchPageRange(false);
+ }
+
+ return;
+ }
+
+ if (e.target == this._rangePicker) {
+ this._rangeInput.hidden = e.target.value != "custom";
+ this.updatePageRange();
+ this.dispatchPageRange();
+ if (!this._rangeInput.hidden) {
+ this._rangeInput.select();
+ }
+ } else if (e.target == this._rangeInput) {
+ this._rangeInput.focus();
+ if (this._numPages) {
+ this.updatePageRange();
+ this.dispatchPageRange();
+ }
+ }
+ }
+}
+customElements.define("page-range-input", PageRangeInput);
+
+class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+
+ this._marginPicker = this.querySelector("#margins-picker");
+ this._customTopMargin = this.querySelector("#custom-margin-top");
+ this._customBottomMargin = this.querySelector("#custom-margin-bottom");
+ this._customLeftMargin = this.querySelector("#custom-margin-left");
+ this._customRightMargin = this.querySelector("#custom-margin-right");
+ this._marginError = this.querySelector("#error-invalid-margin");
+ this._sizeUnit = null;
+ this._toInchesMultiplier = 1;
+ }
+
+ get templateId() {
+ return "margins-template";
+ }
+
+ updateCustomMargins() {
+ let newMargins = {
+ marginTop: this.toInchValue(this._customTopMargin.value),
+ marginBottom: this.toInchValue(this._customBottomMargin.value),
+ marginLeft: this.toInchValue(this._customLeftMargin.value),
+ marginRight: this.toInchValue(this._customRightMargin.value),
+ };
+
+ this.dispatchSettingsChange({
+ margins: "custom",
+ customMargins: newMargins,
+ });
+ this._marginError.hidden = true;
+ }
+
+ updateMaxValues() {
+ let maxWidth = this.toCurrentUnitValue(this._maxWidth);
+ let maxHeight = this.toCurrentUnitValue(this._maxHeight);
+ this._customTopMargin.max = this.formatMaxAttr(
+ maxHeight - this._customBottomMargin.value
+ );
+ this._customBottomMargin.max = this.formatMaxAttr(
+ maxHeight - this._customTopMargin.value
+ );
+ this._customLeftMargin.max = this.formatMaxAttr(
+ maxWidth - this._customRightMargin.value
+ );
+ this._customRightMargin.max = this.formatMaxAttr(
+ maxWidth - this._customLeftMargin.value
+ );
+ }
+
+ truncateTwoDecimals(val) {
+ if (val.split(".")[1].length > 2) {
+ let dotIndex = val.indexOf(".");
+ return val.slice(0, dotIndex + 3);
+ }
+ return val;
+ }
+
+ formatMaxAttr(val) {
+ const strVal = val.toString();
+ if (strVal.includes(".")) {
+ return this.truncateTwoDecimals(strVal);
+ }
+ return val;
+ }
+
+ formatMargin(target) {
+ if (target.value.includes(".")) {
+ target.value = this.truncateTwoDecimals(target.value);
+ }
+ }
+
+ toCurrentUnitValue(val) {
+ if (typeof val == "string") {
+ val = parseFloat(val);
+ }
+ return val / this._toInchesMultiplier;
+ }
+
+ toInchValue(val) {
+ if (typeof val == "string") {
+ val = parseFloat(val);
+ }
+ return val * this._toInchesMultiplier;
+ }
+
+ setAllMarginValues(settings) {
+ this._customTopMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginTop
+ ).toFixed(2);
+ this._customBottomMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginBottom
+ ).toFixed(2);
+ this._customLeftMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginLeft
+ ).toFixed(2);
+ this._customRightMargin.value = this.toCurrentUnitValue(
+ settings.customMargins.marginRight
+ ).toFixed(2);
+ }
+
+ update(settings) {
+ // Re-evaluate which margin options should be enabled whenever the printer or paper changes
+ this._toInchesMultiplier =
+ settings.paperSizeUnit == settings.kPaperSizeMillimeters
+ ? INCHES_PER_MM
+ : 1;
+ if (
+ settings.paperId !== this._paperId ||
+ settings.printerName !== this._printerName ||
+ settings.orientation !== this._orientation
+ ) {
+ let enabledMargins = settings.marginOptions;
+ for (let option of this._marginPicker.options) {
+ option.hidden = !enabledMargins[option.value];
+ }
+ this._paperId = settings.paperId;
+ this._printerName = settings.printerName;
+ this._orientation = settings.orientation;
+
+ // Paper dimensions are in the paperSizeUnit. As the margin values are in inches
+ // we'll normalize to that when storing max dimensions
+ let height =
+ this._orientation == 0 ? settings.paperHeight : settings.paperWidth;
+ let width =
+ this._orientation == 0 ? settings.paperWidth : settings.paperHeight;
+ let heightInches =
+ Math.round(this._toInchesMultiplier * height * 100) / 100;
+ let widthInches =
+ Math.round(this._toInchesMultiplier * width * 100) / 100;
+
+ this._maxHeight =
+ heightInches -
+ settings.unwriteableMarginTop -
+ settings.unwriteableMarginBottom;
+ this._maxWidth =
+ widthInches -
+ settings.unwriteableMarginLeft -
+ settings.unwriteableMarginRight;
+
+ // The values in custom fields should be initialized to custom margin values
+ // and must be overriden if they are no longer valid.
+ this.setAllMarginValues(settings);
+ this.updateMaxValues();
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._marginError.hidden = true;
+ }
+
+ if (settings.paperSizeUnit !== this._sizeUnit) {
+ this._sizeUnit = settings.paperSizeUnit;
+ let unitStr =
+ this._sizeUnit == settings.kPaperSizeMillimeters ? "mm" : "inches";
+ for (let elem of this.querySelectorAll("[data-unit-prefix-l10n-id]")) {
+ let l10nId = elem.getAttribute("data-unit-prefix-l10n-id") + unitStr;
+ elem.setAttribute("data-l10n-id", l10nId);
+ }
+ }
+
+ // We need to ensure we don't override the value if the value should be custom.
+ if (this._marginPicker.value != "custom") {
+ // Reset the custom margin values if they are not valid and revalidate the form
+ if (
+ !this._customTopMargin.checkValidity() ||
+ !this._customBottomMargin.checkValidity() ||
+ !this._customLeftMargin.checkValidity() ||
+ !this._customRightMargin.checkValidity()
+ ) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ this.setAllMarginValues(settings);
+ this.updateMaxValues();
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._marginError.hidden = true;
+ }
+ if (settings.margins == "custom") {
+ // Ensure that we display the custom margin boxes
+ this.querySelector(".margin-group").hidden = false;
+ }
+ this._marginPicker.value = settings.margins;
+ }
+ }
+
+ handleEvent(e) {
+ if (e.target == this._marginPicker) {
+ let customMargin = e.target.value == "custom";
+ this.querySelector(".margin-group").hidden = !customMargin;
+ if (customMargin) {
+ // Update the custom margin values to ensure consistency
+ this.updateCustomMargins();
+ return;
+ }
+
+ this.dispatchSettingsChange({
+ margins: e.target.value,
+ customMargins: null,
+ });
+ }
+
+ if (
+ e.target == this._customTopMargin ||
+ e.target == this._customBottomMargin ||
+ e.target == this._customLeftMargin ||
+ e.target == this._customRightMargin
+ ) {
+ if (e.target.checkValidity()) {
+ this.updateMaxValues();
+ }
+ if (
+ this._customTopMargin.validity.valid &&
+ this._customBottomMargin.validity.valid &&
+ this._customLeftMargin.validity.valid &&
+ this._customRightMargin.validity.valid
+ ) {
+ this.formatMargin(e.target);
+ this.updateCustomMargins();
+ } else if (e.target.validity.stepMismatch) {
+ // If this is the third digit after the decimal point, we should
+ // truncate the string.
+ this.formatMargin(e.target);
+ }
+ }
+
+ window.clearTimeout(this.showErrorTimeoutId);
+ if (
+ this._customTopMargin.validity.valid &&
+ this._customBottomMargin.validity.valid &&
+ this._customLeftMargin.validity.valid &&
+ this._customRightMargin.validity.valid
+ ) {
+ this._marginError.hidden = true;
+ } else {
+ this.cancelSettingsChange({ customMargins: true, margins: true });
+ this.showErrorTimeoutId = window.setTimeout(() => {
+ this._marginError.hidden = false;
+ }, INPUT_DELAY_MS);
+ }
+ }
+}
+customElements.define("margins-select", MarginsPicker);
+
+class TwistySummary extends PrintUIControlMixin(HTMLElement) {
+ get isOpen() {
+ return this.closest("details")?.hasAttribute("open");
+ }
+
+ get templateId() {
+ return "twisty-summary-template";
+ }
+
+ initialize() {
+ if (this._initialized) {
+ return;
+ }
+ super.initialize();
+ this.label = this.querySelector(".label");
+
+ this.addEventListener("click", this);
+ let shouldOpen = Services.prefs.getBoolPref(
+ "print.more-settings.open",
+ false
+ );
+ this.closest("details").open = shouldOpen;
+ this.updateSummary(shouldOpen);
+ }
+
+ handleEvent(e) {
+ let willOpen = !this.isOpen;
+ Services.prefs.setBoolPref("print.more-settings.open", willOpen);
+ this.updateSummary(willOpen);
+ }
+
+ updateSummary(open) {
+ document.l10n.setAttributes(
+ this.label,
+ open
+ ? this.getAttribute("data-open-l10n-id")
+ : this.getAttribute("data-closed-l10n-id")
+ );
+ }
+}
+customElements.define("twisty-summary", TwistySummary);
+
+class PageCount extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+ document.addEventListener("page-count", this);
+ }
+
+ update(settings) {
+ this.numCopies = settings.numCopies;
+ this.duplex = settings.duplex;
+ this.outputDestination = settings.outputDestination;
+ this.render();
+ }
+
+ render() {
+ if (!this.numCopies || !this.sheetCount) {
+ return;
+ }
+
+ let sheetCount = this.sheetCount;
+
+ // When printing to a printer (not to a file) update
+ // the sheet count to account for duplex printing.
+ if (
+ this.outputDestination == Ci.nsIPrintSettings.kOutputDestinationPrinter &&
+ this.duplex != Ci.nsIPrintSettings.kDuplexNone
+ ) {
+ sheetCount = Math.ceil(sheetCount / 2);
+ }
+
+ sheetCount *= this.numCopies;
+
+ document.l10n.setAttributes(this, "printui-sheets-count", {
+ sheetCount,
+ });
+
+ // The loading attribute must be removed on first render
+ if (this.hasAttribute("loading")) {
+ this.removeAttribute("loading");
+ }
+
+ if (this.id) {
+ // We're showing the sheet count, so let it describe the dialog.
+ document.body.setAttribute("aria-describedby", this.id);
+ }
+ }
+
+ handleEvent(e) {
+ this.sheetCount = e.detail.sheetCount;
+ this.render();
+ }
+}
+customElements.define("page-count", PageCount);
+
+class PrintBackgrounds extends PrintSettingCheckbox {
+ update(settings) {
+ super.update(settings);
+ let isSimplified = settings.sourceVersion == "simplified";
+ this.disabled = isSimplified;
+ this.toggleAttribute("disallowed", isSimplified);
+ this.checked = !isSimplified && settings.printBackgrounds;
+ }
+}
+customElements.define("print-backgrounds", PrintBackgrounds, {
+ extends: "input",
+});
+
+class PrintButton extends PrintUIControlMixin(HTMLButtonElement) {
+ update(settings) {
+ let l10nId =
+ settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? "printui-primary-button-save"
+ : "printui-primary-button";
+ document.l10n.setAttributes(this, l10nId);
+ }
+}
+customElements.define("print-button", PrintButton, { extends: "button" });
+
+class CancelButton extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.addEventListener("click", () => {
+ this.dispatchEvent(new Event("cancel-print", { bubbles: true }));
+ });
+ }
+}
+customElements.define("cancel-button", CancelButton, { extends: "button" });
+
+async function pickFileName(contentTitle, currentURI) {
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let [title] = await document.l10n.formatMessages([
+ { id: "printui-save-to-pdf-title" },
+ ]);
+ title = title.value;
+
+ let filename;
+ if (contentTitle != "") {
+ filename = contentTitle;
+ } else {
+ let url = new URL(currentURI);
+ let path = decodeURIComponent(url.pathname);
+ path = path.replace(/\/$/, "");
+ filename = path.split("/").pop();
+ if (filename == "") {
+ filename = url.hostname;
+ }
+ }
+ if (!filename.endsWith(".pdf")) {
+ // macOS and linux don't set the extension based on the default extension.
+ // Windows won't add the extension a second time, fortunately.
+ // If it already ends with .pdf though, adding it again isn't needed.
+ filename += ".pdf";
+ }
+ filename = DownloadPaths.sanitize(filename);
+
+ picker.init(
+ window.docShell.chromeEventHandler.ownerGlobal,
+ title,
+ Ci.nsIFilePicker.modeSave
+ );
+ picker.appendFilter("PDF", "*.pdf");
+ picker.defaultExtension = "pdf";
+ picker.defaultString = filename;
+
+ let retval = await new Promise(resolve => picker.open(resolve));
+
+ if (retval == 1) {
+ throw new Error({ reason: "cancelled" });
+ } else {
+ // OK clicked (retval == 0) or replace confirmed (retval == 2)
+
+ // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
+ // the print progress listener is never called. This workaround ensures that a correct status is always returned.
+ try {
+ let fstream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
+ fstream.close();
+
+ // Remove the file to reduce the likelihood of the user opening an empty or damaged fle when the
+ // preview is loading
+ await IOUtils.remove(picker.file.path);
+ } catch (e) {
+ throw new Error({ reason: retval == 0 ? "not_saved" : "not_replaced" });
+ }
+ }
+
+ return picker.file.path;
+}
diff --git a/toolkit/components/printing/content/printPageSetup.js b/toolkit/components/printing/content/printPageSetup.js
new file mode 100644
index 0000000000..91f5786243
--- /dev/null
+++ b/toolkit/components/printing/content/printPageSetup.js
@@ -0,0 +1,538 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* 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/. */
+
+var gDialog;
+var paramBlock;
+var gPrintService = null;
+var gPrintSettings = null;
+var gStringBundle = null;
+var gDoingMetric = false;
+
+var gPrintSettingsInterface = Ci.nsIPrintSettings;
+var gDoDebug = false;
+
+// ---------------------------------------------------
+function initDialog() {
+ gDialog = {};
+
+ gDialog.orientation = document.getElementById("orientation");
+ gDialog.portrait = document.getElementById("portrait");
+ gDialog.landscape = document.getElementById("landscape");
+
+ gDialog.printBG = document.getElementById("printBG");
+
+ gDialog.shrinkToFit = document.getElementById("shrinkToFit");
+
+ gDialog.marginGroup = document.getElementById("marginGroup");
+
+ gDialog.marginPage = document.getElementById("marginPage");
+ gDialog.marginTop = document.getElementById("marginTop");
+ gDialog.marginBottom = document.getElementById("marginBottom");
+ gDialog.marginLeft = document.getElementById("marginLeft");
+ gDialog.marginRight = document.getElementById("marginRight");
+
+ gDialog.topInput = document.getElementById("topInput");
+ gDialog.bottomInput = document.getElementById("bottomInput");
+ gDialog.leftInput = document.getElementById("leftInput");
+ gDialog.rightInput = document.getElementById("rightInput");
+
+ gDialog.hLeftOption = document.getElementById("hLeftOption");
+ gDialog.hCenterOption = document.getElementById("hCenterOption");
+ gDialog.hRightOption = document.getElementById("hRightOption");
+
+ gDialog.fLeftOption = document.getElementById("fLeftOption");
+ gDialog.fCenterOption = document.getElementById("fCenterOption");
+ gDialog.fRightOption = document.getElementById("fRightOption");
+
+ gDialog.scalingLabel = document.getElementById("scalingInput");
+ gDialog.scalingInput = document.getElementById("scalingInput");
+
+ gDialog.enabled = false;
+
+ document.addEventListener("dialogaccept", onAccept);
+}
+
+// ---------------------------------------------------
+function isListOfPrinterFeaturesAvailable() {
+ return Services.prefs.getBoolPref(
+ "print.tmp.printerfeatures." +
+ gPrintSettings.printerName +
+ ".has_special_printerfeatures",
+ false
+ );
+}
+
+// ---------------------------------------------------
+function checkDouble(element) {
+ element.value = element.value.replace(/[^.0-9]/g, "");
+}
+
+// Theoretical paper width/height.
+var gPageWidth = 8.5;
+var gPageHeight = 11.0;
+
+// ---------------------------------------------------
+function setOrientation() {
+ var selection = gDialog.orientation.selectedItem;
+
+ var style = "background-color:white;";
+ if (
+ (selection == gDialog.portrait && gPageWidth > gPageHeight) ||
+ (selection == gDialog.landscape && gPageWidth < gPageHeight)
+ ) {
+ // Swap width/height.
+ var temp = gPageHeight;
+ gPageHeight = gPageWidth;
+ gPageWidth = temp;
+ }
+ var div = gDoingMetric ? 100 : 10;
+ style +=
+ "width:" +
+ gPageWidth / div +
+ unitString() +
+ ";height:" +
+ gPageHeight / div +
+ unitString() +
+ ";";
+ gDialog.marginPage.setAttribute("style", style);
+}
+
+// ---------------------------------------------------
+function unitString() {
+ return gPrintSettings.paperSizeUnit ==
+ gPrintSettingsInterface.kPaperSizeInches
+ ? "in"
+ : "mm";
+}
+
+// ---------------------------------------------------
+function checkMargin(value, max, other) {
+ // Don't draw this margin bigger than permitted.
+ return Math.min(value, max - other.value);
+}
+
+// ---------------------------------------------------
+function changeMargin(node) {
+ // Correct invalid input.
+ checkDouble(node);
+
+ // Reset the margin height/width for this node.
+ var val = node.value;
+ var nodeToStyle;
+ var attr = "width";
+ if (node == gDialog.topInput) {
+ nodeToStyle = gDialog.marginTop;
+ val = checkMargin(val, gPageHeight, gDialog.bottomInput);
+ attr = "height";
+ } else if (node == gDialog.bottomInput) {
+ nodeToStyle = gDialog.marginBottom;
+ val = checkMargin(val, gPageHeight, gDialog.topInput);
+ attr = "height";
+ } else if (node == gDialog.leftInput) {
+ nodeToStyle = gDialog.marginLeft;
+ val = checkMargin(val, gPageWidth, gDialog.rightInput);
+ } else {
+ nodeToStyle = gDialog.marginRight;
+ val = checkMargin(val, gPageWidth, gDialog.leftInput);
+ }
+ var style = attr + ":" + val / 10 + unitString() + ";";
+ nodeToStyle.setAttribute("style", style);
+}
+
+// ---------------------------------------------------
+function changeMargins() {
+ changeMargin(gDialog.topInput);
+ changeMargin(gDialog.bottomInput);
+ changeMargin(gDialog.leftInput);
+ changeMargin(gDialog.rightInput);
+}
+
+// ---------------------------------------------------
+async function customize(node) {
+ // If selection is now "Custom..." then prompt user for custom setting.
+ if (node.value == 6) {
+ let [title, promptText] = await document.l10n.formatValues([
+ { id: "custom-prompt-title" },
+ { id: "custom-prompt-prompt" },
+ ]);
+ var result = { value: node.custom };
+ var ok = Services.prompt.prompt(window, title, promptText, result, null, {
+ value: false,
+ });
+ if (ok) {
+ node.custom = result.value;
+ }
+ }
+}
+
+// ---------------------------------------------------
+function setHeaderFooter(node, value) {
+ node.value = hfValueToId(value);
+ if (node.value == 6) {
+ // Remember current Custom... value.
+ node.custom = value;
+ } else {
+ // Start with empty Custom... value.
+ node.custom = "";
+ }
+}
+
+var gHFValues = [];
+gHFValues["&T"] = 1;
+gHFValues["&U"] = 2;
+gHFValues["&D"] = 3;
+gHFValues["&P"] = 4;
+gHFValues["&PT"] = 5;
+
+function hfValueToId(val) {
+ if (val in gHFValues) {
+ return gHFValues[val];
+ }
+ if (val.length) {
+ return 6; // Custom...
+ }
+ return 0; // --blank--
+}
+
+function hfIdToValue(node) {
+ var result = "";
+ switch (parseInt(node.value)) {
+ case 0:
+ break;
+ case 1:
+ result = "&T";
+ break;
+ case 2:
+ result = "&U";
+ break;
+ case 3:
+ result = "&D";
+ break;
+ case 4:
+ result = "&P";
+ break;
+ case 5:
+ result = "&PT";
+ break;
+ case 6:
+ result = node.custom;
+ break;
+ }
+ return result;
+}
+
+async function lastUsedPrinterNameOrDefault() {
+ let printerList = Cc["@mozilla.org/gfx/printerlist;1"].getService(
+ Ci.nsIPrinterList
+ );
+ let lastUsedName = gPrintService.lastUsedPrinterName;
+ let printers = await printerList.printers;
+ for (let printer of printers) {
+ printer.QueryInterface(Ci.nsIPrinter);
+ if (printer.name == lastUsedName) {
+ return lastUsedName;
+ }
+ }
+ return printerList.systemDefaultPrinterName;
+}
+
+async function setPrinterDefaultsForSelectedPrinter() {
+ if (gPrintSettings.printerName == "") {
+ gPrintSettings.printerName = await lastUsedPrinterNameOrDefault();
+ }
+
+ // First get any defaults from the printer
+ gPrintService.initPrintSettingsFromPrinter(
+ gPrintSettings.printerName,
+ gPrintSettings
+ );
+
+ // now augment them with any values from last time
+ gPrintService.initPrintSettingsFromPrefs(
+ gPrintSettings,
+ true,
+ gPrintSettingsInterface.kInitSaveAll
+ );
+
+ if (gDoDebug) {
+ dump(
+ "pagesetup/setPrinterDefaultsForSelectedPrinter: printerName='" +
+ gPrintSettings.printerName +
+ "', orientation='" +
+ gPrintSettings.orientation +
+ "'\n"
+ );
+ }
+}
+
+// ---------------------------------------------------
+async function loadDialog() {
+ var print_orientation = 0;
+ var print_margin_top = 0.5;
+ var print_margin_left = 0.5;
+ var print_margin_bottom = 0.5;
+ var print_margin_right = 0.5;
+
+ try {
+ gPrintService = Cc["@mozilla.org/gfx/printsettings-service;1"];
+ if (gPrintService) {
+ gPrintService = gPrintService.getService();
+ if (gPrintService) {
+ gPrintService = gPrintService.QueryInterface(
+ Ci.nsIPrintSettingsService
+ );
+ }
+ }
+ } catch (ex) {
+ dump("loadDialog: ex=" + ex + "\n");
+ }
+
+ await setPrinterDefaultsForSelectedPrinter();
+
+ gDialog.printBG.checked =
+ gPrintSettings.printBGColors || gPrintSettings.printBGImages;
+
+ gDialog.shrinkToFit.checked = gPrintSettings.shrinkToFit;
+
+ gDialog.scalingLabel.disabled = gDialog.scalingInput.disabled =
+ gDialog.shrinkToFit.checked;
+
+ if (
+ gPrintSettings.paperSizeUnit == gPrintSettingsInterface.kPaperSizeInches
+ ) {
+ document.l10n.setAttributes(
+ gDialog.marginGroup,
+ "margin-group-label-inches"
+ );
+ gDoingMetric = false;
+ } else {
+ document.l10n.setAttributes(
+ gDialog.marginGroup,
+ "margin-group-label-metric"
+ );
+ // Also, set global page dimensions for A4 paper, in millimeters (assumes portrait at this point).
+ gPageWidth = 2100;
+ gPageHeight = 2970;
+ gDoingMetric = true;
+ }
+
+ print_orientation = gPrintSettings.orientation;
+ print_margin_top = convertMarginInchesToUnits(
+ gPrintSettings.marginTop,
+ gDoingMetric
+ );
+ print_margin_left = convertMarginInchesToUnits(
+ gPrintSettings.marginLeft,
+ gDoingMetric
+ );
+ print_margin_right = convertMarginInchesToUnits(
+ gPrintSettings.marginRight,
+ gDoingMetric
+ );
+ print_margin_bottom = convertMarginInchesToUnits(
+ gPrintSettings.marginBottom,
+ gDoingMetric
+ );
+
+ if (gDoDebug) {
+ dump("print_orientation " + print_orientation + "\n");
+
+ dump("print_margin_top " + print_margin_top + "\n");
+ dump("print_margin_left " + print_margin_left + "\n");
+ dump("print_margin_right " + print_margin_right + "\n");
+ dump("print_margin_bottom " + print_margin_bottom + "\n");
+ }
+
+ if (print_orientation == gPrintSettingsInterface.kPortraitOrientation) {
+ gDialog.orientation.selectedItem = gDialog.portrait;
+ } else if (
+ print_orientation == gPrintSettingsInterface.kLandscapeOrientation
+ ) {
+ gDialog.orientation.selectedItem = gDialog.landscape;
+ }
+
+ // Set orientation the first time on a timeout so the dialog sizes to the
+ // maximum height specified in the .xul file. Otherwise, if the user switches
+ // from landscape to portrait, the content grows and the buttons are clipped.
+ setTimeout(setOrientation, 0);
+
+ gDialog.topInput.value = print_margin_top.toFixed(1);
+ gDialog.bottomInput.value = print_margin_bottom.toFixed(1);
+ gDialog.leftInput.value = print_margin_left.toFixed(1);
+ gDialog.rightInput.value = print_margin_right.toFixed(1);
+ changeMargins();
+
+ setHeaderFooter(gDialog.hLeftOption, gPrintSettings.headerStrLeft);
+ setHeaderFooter(gDialog.hCenterOption, gPrintSettings.headerStrCenter);
+ setHeaderFooter(gDialog.hRightOption, gPrintSettings.headerStrRight);
+
+ setHeaderFooter(gDialog.fLeftOption, gPrintSettings.footerStrLeft);
+ setHeaderFooter(gDialog.fCenterOption, gPrintSettings.footerStrCenter);
+ setHeaderFooter(gDialog.fRightOption, gPrintSettings.footerStrRight);
+
+ gDialog.scalingInput.value = (gPrintSettings.scaling * 100).toFixed(0);
+
+ // Enable/disable widgets based in the information whether the selected
+ // printer supports the matching feature or not
+ if (isListOfPrinterFeaturesAvailable()) {
+ if (
+ Services.prefs.getBoolPref(
+ "print.tmp.printerfeatures." +
+ gPrintSettings.printerName +
+ ".can_change_orientation"
+ )
+ ) {
+ gDialog.orientation.removeAttribute("disabled");
+ } else {
+ gDialog.orientation.setAttribute("disabled", "true");
+ }
+ }
+
+ // Give initial focus to the orientation radio group.
+ // Done on a timeout due to to bug 103197.
+ setTimeout(function () {
+ gDialog.orientation.focus();
+ }, 0);
+}
+
+// ---------------------------------------------------
+function onLoad() {
+ // Init gDialog.
+ initDialog();
+
+ if (window.arguments[0] != null) {
+ gPrintSettings = window.arguments[0].QueryInterface(Ci.nsIPrintSettings);
+ paramBlock = window.arguments[1].QueryInterface(Ci.nsIDialogParamBlock);
+ } else if (gDoDebug) {
+ alert("window.arguments[0] == null!");
+ }
+
+ // default return value is "cancel"
+ paramBlock.SetInt(0, 0);
+
+ if (gPrintSettings) {
+ loadDialog();
+ } else if (gDoDebug) {
+ alert("Could initialize gDialog, PrintSettings is null!");
+ }
+}
+
+function convertUnitsMarginToInches(aVal, aIsMetric) {
+ if (aIsMetric) {
+ return aVal / 25.4;
+ }
+ return aVal;
+}
+
+function convertMarginInchesToUnits(aVal, aIsMetric) {
+ if (aIsMetric) {
+ return aVal * 25.4;
+ }
+ return aVal;
+}
+
+// ---------------------------------------------------
+function onAccept() {
+ if (gPrintSettings) {
+ if (gDialog.orientation.selectedItem == gDialog.portrait) {
+ gPrintSettings.orientation = gPrintSettingsInterface.kPortraitOrientation;
+ } else {
+ gPrintSettings.orientation =
+ gPrintSettingsInterface.kLandscapeOrientation;
+ }
+
+ // save these out so they can be picked up by the device spec
+ gPrintSettings.marginTop = convertUnitsMarginToInches(
+ gDialog.topInput.value,
+ gDoingMetric
+ );
+ gPrintSettings.marginLeft = convertUnitsMarginToInches(
+ gDialog.leftInput.value,
+ gDoingMetric
+ );
+ gPrintSettings.marginBottom = convertUnitsMarginToInches(
+ gDialog.bottomInput.value,
+ gDoingMetric
+ );
+ gPrintSettings.marginRight = convertUnitsMarginToInches(
+ gDialog.rightInput.value,
+ gDoingMetric
+ );
+
+ gPrintSettings.headerStrLeft = hfIdToValue(gDialog.hLeftOption);
+ gPrintSettings.headerStrCenter = hfIdToValue(gDialog.hCenterOption);
+ gPrintSettings.headerStrRight = hfIdToValue(gDialog.hRightOption);
+
+ gPrintSettings.footerStrLeft = hfIdToValue(gDialog.fLeftOption);
+ gPrintSettings.footerStrCenter = hfIdToValue(gDialog.fCenterOption);
+ gPrintSettings.footerStrRight = hfIdToValue(gDialog.fRightOption);
+
+ gPrintSettings.printBGColors = gDialog.printBG.checked;
+ gPrintSettings.printBGImages = gDialog.printBG.checked;
+
+ gPrintSettings.shrinkToFit = gDialog.shrinkToFit.checked;
+
+ var scaling = document.getElementById("scalingInput").value;
+ if (scaling < 10.0) {
+ scaling = 10.0;
+ }
+ if (scaling > 500.0) {
+ scaling = 500.0;
+ }
+ scaling /= 100.0;
+ gPrintSettings.scaling = scaling;
+
+ if (gDoDebug) {
+ dump("******* Page Setup Accepting ******\n");
+ dump("print_margin_top " + gDialog.topInput.value + "\n");
+ dump("print_margin_left " + gDialog.leftInput.value + "\n");
+ dump("print_margin_right " + gDialog.bottomInput.value + "\n");
+ dump("print_margin_bottom " + gDialog.rightInput.value + "\n");
+ }
+ }
+
+ // set return value to "ok"
+ if (paramBlock) {
+ paramBlock.SetInt(0, 1);
+ } else {
+ dump("*** FATAL ERROR: No paramBlock\n");
+ }
+
+ var flags =
+ gPrintSettingsInterface.kInitSaveMargins |
+ gPrintSettingsInterface.kInitSaveHeaderLeft |
+ gPrintSettingsInterface.kInitSaveHeaderCenter |
+ gPrintSettingsInterface.kInitSaveHeaderRight |
+ gPrintSettingsInterface.kInitSaveFooterLeft |
+ gPrintSettingsInterface.kInitSaveFooterCenter |
+ gPrintSettingsInterface.kInitSaveFooterRight |
+ gPrintSettingsInterface.kInitSaveBGColors |
+ gPrintSettingsInterface.kInitSaveBGImages |
+ gPrintSettingsInterface.kInitSaveInColor |
+ gPrintSettingsInterface.kInitSaveReversed |
+ gPrintSettingsInterface.kInitSaveOrientation |
+ gPrintSettingsInterface.kInitSaveShrinkToFit |
+ gPrintSettingsInterface.kInitSaveScaling;
+
+ // Note that this file is Windows only code, so this doesn't handle saving
+ // for other platforms.
+ // XXX Should we do this in nsPrintDialogServiceWin::ShowPageSetup (the code
+ // that invokes us), since ShowPageSetup is where we do the saving for the
+ // other platforms?
+ gPrintService.maybeSavePrintSettingsToPrefs(gPrintSettings, flags);
+}
+
+// ---------------------------------------------------
+function onCancel() {
+ // set return value to "cancel"
+ if (paramBlock) {
+ paramBlock.SetInt(0, 0);
+ } else {
+ dump("*** FATAL ERROR: No paramBlock\n");
+ }
+
+ return true;
+}
diff --git a/toolkit/components/printing/content/printPageSetup.xhtml b/toolkit/components/printing/content/printPageSetup.xhtml
new file mode 100644
index 0000000000..235e8a474b
--- /dev/null
+++ b/toolkit/components/printing/content/printPageSetup.xhtml
@@ -0,0 +1,291 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: HTML -*- -->
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?>
+<?xml-stylesheet href="chrome://global/skin/printPageSetup.css" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="onLoad();"
+ oncancel="return onCancel();"
+ data-l10n-id="print-setup"
+ dialogroot="true"
+ persist="screenX screenY"
+ screenX="24"
+ screenY="24"
+>
+ <dialog id="printPageSetupDialog">
+ <linkset>
+ <html:link rel="localization" href="toolkit/printing/printDialogs.ftl" />
+ </linkset>
+
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://global/content/printPageSetup.js" />
+
+ <tabbox flex="1">
+ <tabs>
+ <tab data-l10n-id="basic-tab" />
+ <tab data-l10n-id="advanced-tab" />
+ </tabs>
+ <tabpanels flex="1">
+ <vbox>
+ <html:fieldset>
+ <html:legend
+ ><label data-l10n-id="format-group-label"
+ /></html:legend>
+ <vbox class="groupbox-body">
+ <hbox align="center">
+ <label control="orientation" data-l10n-id="orientation-label" />
+ <radiogroup id="orientation" oncommand="setOrientation()">
+ <hbox align="center">
+ <radio
+ id="portrait"
+ class="portrait-page"
+ data-l10n-id="portrait"
+ />
+ <radio
+ id="landscape"
+ class="landscape-page"
+ data-l10n-id="landscape"
+ />
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <separator />
+ <hbox align="center">
+ <label control="scalingInput" data-l10n-id="scale" />
+ <html:input
+ id="scalingInput"
+ size="4"
+ oninput="checkDouble(this)"
+ />
+ <label data-l10n-id="scale-percent" />
+ <separator />
+ <checkbox
+ id="shrinkToFit"
+ data-l10n-id="shrink-to-fit"
+ oncommand="gDialog.scalingInput.disabled = gDialog.scalingLabel.disabled = this.checked"
+ />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ <html:fieldset>
+ <html:legend
+ ><label data-l10n-id="options-group-label"
+ /></html:legend>
+ <checkbox
+ id="printBG"
+ class="groupbox-body"
+ data-l10n-id="print-bg"
+ />
+ </html:fieldset>
+ </vbox>
+ <vbox>
+ <html:fieldset>
+ <html:legend
+ ><label id="marginGroup" data-l10n-id="margin-group-label"
+ /></html:legend>
+ <vbox class="groupbox-body">
+ <hbox align="center">
+ <spacer flex="1" />
+ <label control="topInput" data-l10n-id="margin-top" />
+ <html:input
+ id="topInput"
+ size="5"
+ oninput="changeMargin(this)"
+ />
+ <!-- This invisible label (with same content as the visible one!) is used
+ to ensure that the <input> is centered above the page. The same
+ technique is deployed for the bottom/left/right input fields, below. -->
+ <label
+ data-l10n-id="margin-top-invisible"
+ style="visibility: hidden"
+ />
+ <spacer flex="1" />
+ </hbox>
+ <hbox dir="ltr">
+ <spacer flex="1" />
+ <vbox>
+ <spacer flex="1" />
+ <label control="leftInput" data-l10n-id="margin-left" />
+ <html:input
+ id="leftInput"
+ size="5"
+ oninput="changeMargin(this)"
+ />
+ <label
+ data-l10n-id="margin-left-invisible"
+ style="visibility: hidden"
+ />
+ <spacer flex="1" />
+ </vbox>
+ <!-- The "margin page" draws a simulated printout page with dashed lines
+ for the margins. The height/width style attributes of the marginTop,
+ marginBottom, marginLeft, and marginRight elements are set by
+ the JS code dynamically based on the user input. -->
+ <vbox id="marginPage" style="height: 29.7mm">
+ <box id="marginTop" style="height: 0.05in" />
+ <hbox flex="1" dir="ltr">
+ <box id="marginLeft" style="width: 0.025in" />
+ <box
+ style="
+ border: 1px;
+ border-style: dashed;
+ border-color: gray;
+ "
+ flex="1"
+ />
+ <box id="marginRight" style="width: 0.025in" />
+ </hbox>
+ <box id="marginBottom" style="height: 0.05in" />
+ </vbox>
+ <vbox>
+ <spacer flex="1" />
+ <label control="rightInput" data-l10n-id="margin-right" />
+ <html:input
+ id="rightInput"
+ size="5"
+ oninput="changeMargin(this)"
+ />
+ <label
+ data-l10n-id="margin-right-invisible"
+ style="visibility: hidden"
+ />
+ <spacer flex="1" />
+ </vbox>
+ <spacer flex="1" />
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1" />
+ <label control="bottomInput" data-l10n-id="margin-bottom" />
+ <html:input
+ id="bottomInput"
+ size="5"
+ oninput="changeMargin(this)"
+ />
+ <label
+ data-l10n-id="margin-bottom-invisible"
+ style="visibility: hidden"
+ />
+ <spacer flex="1" />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ <html:fieldset>
+ <html:legend
+ ><label data-l10n-id="header-footer-label"
+ /></html:legend>
+ <box id="header-footer-grid" class="groupbox-body" dir="ltr">
+ <menulist
+ id="hLeftOption"
+ oncommand="customize(this)"
+ data-l10n-id="header-left-tip"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="hf-blank" />
+ <menuitem value="1" data-l10n-id="hf-title" />
+ <menuitem value="2" data-l10n-id="hf-url" />
+ <menuitem value="3" data-l10n-id="hf-date-and-time" />
+ <menuitem value="4" data-l10n-id="hf-page" />
+ <menuitem value="5" data-l10n-id="hf-page-and-total" />
+ <menuitem value="6" data-l10n-id="hf-custom" />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="hCenterOption"
+ oncommand="customize(this)"
+ data-l10n-id="header-center-tip"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="hf-blank" />
+ <menuitem value="1" data-l10n-id="hf-title" />
+ <menuitem value="2" data-l10n-id="hf-url" />
+ <menuitem value="3" data-l10n-id="hf-date-and-time" />
+ <menuitem value="4" data-l10n-id="hf-page" />
+ <menuitem value="5" data-l10n-id="hf-page-and-total" />
+ <menuitem value="6" data-l10n-id="hf-custom" />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="hRightOption"
+ oncommand="customize(this)"
+ data-l10n-id="header-right-tip"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="hf-blank" />
+ <menuitem value="1" data-l10n-id="hf-title" />
+ <menuitem value="2" data-l10n-id="hf-url" />
+ <menuitem value="3" data-l10n-id="hf-date-and-time" />
+ <menuitem value="4" data-l10n-id="hf-page" />
+ <menuitem value="5" data-l10n-id="hf-page-and-total" />
+ <menuitem value="6" data-l10n-id="hf-custom" />
+ </menupopup>
+ </menulist>
+ <vbox align="center">
+ <label data-l10n-id="hf-left-label" />
+ </vbox>
+ <vbox align="center">
+ <label data-l10n-id="hf-center-label" />
+ </vbox>
+ <vbox align="center">
+ <label data-l10n-id="hf-right-label" />
+ </vbox>
+ <menulist
+ id="fLeftOption"
+ oncommand="customize(this)"
+ data-l10n-id="footer-left-tip"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="hf-blank" />
+ <menuitem value="1" data-l10n-id="hf-title" />
+ <menuitem value="2" data-l10n-id="hf-url" />
+ <menuitem value="3" data-l10n-id="hf-date-and-time" />
+ <menuitem value="4" data-l10n-id="hf-page" />
+ <menuitem value="5" data-l10n-id="hf-page-and-total" />
+ <menuitem value="6" data-l10n-id="hf-custom" />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="fCenterOption"
+ oncommand="customize(this)"
+ data-l10n-id="footer-center-tip"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="hf-blank" />
+ <menuitem value="1" data-l10n-id="hf-title" />
+ <menuitem value="2" data-l10n-id="hf-url" />
+ <menuitem value="3" data-l10n-id="hf-date-and-time" />
+ <menuitem value="4" data-l10n-id="hf-page" />
+ <menuitem value="5" data-l10n-id="hf-page-and-total" />
+ <menuitem value="6" data-l10n-id="hf-custom" />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="fRightOption"
+ oncommand="customize(this)"
+ data-l10n-id="footer-right-tip"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="hf-blank" />
+ <menuitem value="1" data-l10n-id="hf-title" />
+ <menuitem value="2" data-l10n-id="hf-url" />
+ <menuitem value="3" data-l10n-id="hf-date-and-time" />
+ <menuitem value="4" data-l10n-id="hf-page" />
+ <menuitem value="5" data-l10n-id="hf-page-and-total" />
+ <menuitem value="6" data-l10n-id="hf-custom" />
+ </menupopup>
+ </menulist>
+ </box>
+ </html:fieldset>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+</window>
diff --git a/toolkit/components/printing/content/printPagination.css b/toolkit/components/printing/content/printPagination.css
new file mode 100644
index 0000000000..5eb7b46924
--- /dev/null
+++ b/toolkit/components/printing/content/printPagination.css
@@ -0,0 +1,141 @@
+/* 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/. */
+
+:host {
+ /* in-content/common.css variables */
+ --blue-50: #0a84ff;
+ --grey-90-a10: rgba(12, 12, 13, 0.1);
+ --shadow-30: 0 4px 16px var(--grey-90-a10);
+ --border-active-shadow: var(--blue-50);
+}
+
+:host {
+ display: block;
+ position: absolute;
+ bottom: 24px;
+ inset-inline-start: 50%;
+ translate: -50%;
+}
+:host(:-moz-locale-dir(rtl)) {
+ translate: 50%;
+}
+
+.container {
+ margin-inline: auto;
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ box-shadow: var(--shadow-30);
+ color: var(--toolbar-color);
+ background-color: var(--toolbar-bgcolor);
+ border-radius: 6px;
+ border-style: none;
+}
+.container::before {
+ content: "";
+ display: block;
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background-color: ButtonFace;
+ border-radius: 6px;
+}
+
+.toolbarButton,
+.toolbarCenter {
+ align-self: stretch;
+ flex: 0 0 auto;
+ padding: var(--toolbarbutton-outer-padding);
+ border: none;
+ border-inline-end: 1px solid ThreeDShadow;
+ border-block: 1px solid ThreeDShadow;
+ color: inherit;
+ background-color: transparent;
+ min-width: calc(2 * var(--toolbarbutton-inner-padding) + 16px);
+ min-height: calc(2 * var(--toolbarbutton-inner-padding) + 16px);
+}
+.startItem {
+ border-inline-start: 1px solid ThreeDShadow;
+ border-start-start-radius: 6px;
+ border-end-start-radius: 6px;
+}
+.endItem {
+ border-start-end-radius: 6px;
+ border-end-end-radius: 6px;
+}
+
+.toolbarButton::after {
+ content: "";
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ vertical-align: text-bottom;
+ text-align: center;
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 12px;
+ -moz-context-properties: fill, fill-opacity;
+ fill: var(--toolbarbutton-icon-fill);
+}
+
+.toolbarButton:hover {
+ background-color: var(--toolbarbutton-hover-background);
+}
+.toolbarButton:hover:active {
+ background-color: var(--toolbarbutton-active-background);
+}
+.toolbarButton::-moz-focus-inner {
+ border: none;
+}
+.toolbarButton:focus {
+ z-index: 1;
+}
+
+.toolbarButton:-moz-focusring {
+ outline: 2px solid var(--border-active-shadow);
+}
+
+.toolbarCenter {
+ flex-shrink: 0;
+ /* 3 chars + (3px border + 1px padding) on both sides */
+ min-width: calc(8px + 3ch);
+ padding: 0 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#navigateHome::after,
+#navigateEnd::after {
+ background-image: url("chrome://global/skin/icons/chevron.svg");
+}
+
+#navigatePrevious::after,
+#navigateNext::after {
+ background-image: url("chrome://global/skin/icons/arrow-left.svg");
+}
+
+#navigatePrevious:-moz-locale-dir(rtl)::after,
+#navigateNext:-moz-locale-dir(ltr)::after {
+ background-image: url("chrome://global/skin/icons/arrow-right.svg");
+}
+
+#navigateEnd:-moz-locale-dir(rtl)::after,
+#navigateHome:-moz-locale-dir(ltr)::after {
+ transform: scaleX(-1);
+}
+
+/* progressively hide the navigation buttons when the print preview is too narrow to fit */
+@media (max-width: 550px) {
+ #navigatePrevious,
+ #navigateNext,
+ #navigateEnd,
+ #navigateHome {
+ display: none;
+ }
+ .toolbarCenter {
+ border-inline-start: 1px solid ThreeDShadow;
+ border-radius: 6px;
+ }
+}
diff --git a/toolkit/components/printing/content/printPreviewPagination.js b/toolkit/components/printing/content/printPreviewPagination.js
new file mode 100644
index 0000000000..c503f0a8cb
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewPagination.js
@@ -0,0 +1,185 @@
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* 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/. */
+
+customElements.define(
+ "printpreview-pagination",
+ class PrintPreviewPagination extends HTMLElement {
+ static get markup() {
+ return `
+ <html:link rel="stylesheet" href="chrome://global/content/printPagination.css" />
+ <html:div class="container">
+ <html:button id="navigateHome" class="toolbarButton startItem" data-l10n-id="printpreview-homearrow-button"></html:button>
+ <html:button id="navigatePrevious" class="toolbarButton" data-l10n-id="printpreview-previousarrow-button"></html:button>
+ <html:div class="toolbarCenter"><html:span id="sheetIndicator" data-l10n-id="printpreview-sheet-of-sheets" data-l10n-args='{ "sheetNum": 1, "sheetCount": 1 }'></html:span></html:div>
+ <html:button id="navigateNext" class="toolbarButton" data-l10n-id="printpreview-nextarrow-button"></html:button>
+ <html:button id="navigateEnd" class="toolbarButton endItem" data-l10n-id="printpreview-endarrow-button"></html:button>
+ </html:div>
+ `;
+ }
+
+ static get defaultProperties() {
+ return {
+ currentPage: 1,
+ sheetCount: 1,
+ };
+ }
+
+ get previewBrowser() {
+ return this._previewBrowser;
+ }
+
+ set previewBrowser(aBrowser) {
+ this._previewBrowser = aBrowser;
+ }
+
+ observePreviewBrowser(browser) {
+ if (browser == this.previewBrowser || !this.isConnected) {
+ return;
+ }
+ this.previewBrowser = browser;
+ this.mutationObserver.disconnect();
+ this.mutationObserver.observe(browser, {
+ attributes: ["current-page", "sheet-count"],
+ });
+ this.updateFromBrowser();
+ }
+
+ connectedCallback() {
+ MozXULElement.insertFTLIfNeeded("toolkit/printing/printPreview.ftl");
+
+ const shadowRoot = this.attachShadow({ mode: "open" });
+ document.l10n.connectRoot(shadowRoot);
+
+ let fragment = MozXULElement.parseXULToFragment(this.constructor.markup);
+ this.shadowRoot.append(fragment);
+
+ this.elements = {
+ sheetIndicator: shadowRoot.querySelector("#sheetIndicator"),
+ homeButton: shadowRoot.querySelector("#navigateHome"),
+ previousButton: shadowRoot.querySelector("#navigatePrevious"),
+ nextButton: shadowRoot.querySelector("#navigateNext"),
+ endButton: shadowRoot.querySelector("#navigateEnd"),
+ };
+
+ this.shadowRoot.addEventListener("click", this);
+
+ this.mutationObserver = new MutationObserver(() =>
+ this.updateFromBrowser()
+ );
+
+ // Initial render with some default values
+ // We'll be updated with real values when available
+ this.update(this.constructor.defaultProperties);
+ }
+
+ disconnectedCallback() {
+ document.l10n.disconnectRoot(this.shadowRoot);
+ this.shadowRoot.textContent = "";
+ this.mutationObserver?.disconnect();
+ delete this.mutationObserver;
+ this.currentPreviewBrowserObserver?.disconnect();
+ delete this.currentPreviewBrowserObserver;
+ }
+
+ handleEvent(event) {
+ if (event.type == "click" && event.button != 0) {
+ return;
+ }
+ event.stopPropagation();
+
+ switch (event.target) {
+ case this.elements.homeButton:
+ this.navigate(0, 0, "home");
+ break;
+ case this.elements.previousButton:
+ this.navigate(-1, 0, 0);
+ break;
+ case this.elements.nextButton:
+ this.navigate(1, 0, 0);
+ break;
+ case this.elements.endButton:
+ this.navigate(0, 0, "end");
+ break;
+ }
+ }
+
+ navigate(aDirection, aPageNum, aHomeOrEnd) {
+ const nsIWebBrowserPrint = Ci.nsIWebBrowserPrint;
+ let targetNum;
+ let navType;
+ // we use only one of aHomeOrEnd, aDirection, or aPageNum
+ if (aHomeOrEnd) {
+ // We're going to either the very first page ("home"), or the
+ // very last page ("end").
+ if (aHomeOrEnd == "home") {
+ targetNum = 1;
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_HOME;
+ } else {
+ targetNum = this.sheetCount;
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_END;
+ }
+ } else if (aPageNum) {
+ // We're going to a specific page (aPageNum)
+ targetNum = Math.min(Math.max(1, aPageNum), this.sheetCount);
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM;
+ } else {
+ // aDirection is either +1 or -1, and allows us to increment
+ // or decrement our currently viewed page.
+ targetNum = Math.min(
+ Math.max(1, this.currentSheet + aDirection),
+ this.sheetCount
+ );
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM;
+ }
+
+ // Preemptively update our own state, rather than waiting for the message from the child process
+ // This allows subsequent clicks of next/back to advance 1 page per click if possible
+ // and keeps the UI feeling more responsive
+ this.update({ currentPage: targetNum });
+
+ this.previewBrowser.sendMessageToActor(
+ "Printing:Preview:Navigate",
+ {
+ navType,
+ pageNum: targetNum,
+ },
+ "Printing"
+ );
+ }
+
+ update(data = {}) {
+ if (data.sheetCount) {
+ this.sheetCount = data.sheetCount;
+ }
+ if (data.currentPage) {
+ this.currentSheet = data.currentPage;
+ }
+ document.l10n.setAttributes(
+ this.elements.sheetIndicator,
+ this.elements.sheetIndicator.dataset.l10nId,
+ {
+ sheetNum: this.currentSheet,
+ sheetCount: this.sheetCount,
+ }
+ );
+ }
+
+ updateFromBrowser() {
+ let sheetCount = parseInt(
+ this.previewBrowser.getAttribute("sheet-count"),
+ 10
+ );
+ let currentPage = parseInt(
+ this.previewBrowser.getAttribute("current-page"),
+ 10
+ );
+ this.update({ sheetCount, currentPage });
+ }
+ }
+);
diff --git a/toolkit/components/printing/content/printUtils.js b/toolkit/components/printing/content/printUtils.js
new file mode 100644
index 0000000000..f1e3489fe7
--- /dev/null
+++ b/toolkit/components/printing/content/printUtils.js
@@ -0,0 +1,822 @@
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* 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/. */
+
+/**
+ * PrintUtils is a utility for front-end code to trigger common print
+ * operations (printing, show print preview, show page settings).
+ *
+ * Unfortunately, likely due to inconsistencies in how different operating
+ * systems do printing natively, our XPCOM-level printing interfaces
+ * are a bit confusing and the method by which we do something basic
+ * like printing a page is quite circuitous.
+ *
+ * To compound that, we need to support remote browsers, and that means
+ * kicking off the print jobs in the content process. This means we send
+ * messages back and forth to that process via the Printing actor.
+ *
+ * This also means that <xul:browser>'s that hope to use PrintUtils must have
+ * their type attribute set to "content".
+ *
+ * Messages sent:
+ *
+ * Printing:Preview:Enter
+ * This message is sent to put content into print preview mode. We pass
+ * the content window of the browser we're showing the preview of, and
+ * the target of the message is the browser that we'll be showing the
+ * preview in.
+ *
+ * Printing:Preview:Exit
+ * This message is sent to take content out of print preview mode.
+ */
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "SHOW_PAGE_SETUP_MENU",
+ "print.show_page_setup_menu",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "PRINT_ALWAYS_SILENT",
+ "print.always_print_silent",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "PREFER_SYSTEM_DIALOG",
+ "print.prefer_system_dialog",
+ false
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
+});
+
+var PrintUtils = {
+ SAVE_TO_PDF_PRINTER: "Mozilla Save to PDF",
+
+ get _bundle() {
+ delete this._bundle;
+ return (this._bundle = Services.strings.createBundle(
+ "chrome://global/locale/printing.properties"
+ ));
+ },
+
+ async checkForSelection(browsingContext) {
+ try {
+ let sourceActor =
+ browsingContext.currentWindowGlobal.getActor("PrintingSelection");
+ // Need the await for the try to trigger...
+ return await sourceActor.sendQuery("PrintingSelection:HasSelection", {});
+ } catch (e) {
+ console.error(e);
+ }
+ return false;
+ },
+
+ /**
+ * Updates the hidden state of the "Page Setup" menu items in the File menu,
+ * depending on the value of the `print.show_page_setup_menu` pref.
+ * Note: not all platforms have a "Page Setup" menu item (or Page Setup
+ * window).
+ */
+ updatePrintSetupMenuHiddenState() {
+ let pageSetupMenuItem = document.getElementById("menu_printSetup");
+ if (pageSetupMenuItem) {
+ pageSetupMenuItem.hidden = !SHOW_PAGE_SETUP_MENU;
+ }
+ },
+
+ /**
+ * Shows the page setup dialog, and saves any settings changed in
+ * that dialog if print.save_print_settings is set to true.
+ *
+ * @return true on success, false on failure
+ */
+ showPageSetup() {
+ let printSettings = this.getPrintSettings();
+ // If we come directly from the Page Setup menu, the hack in
+ // _enterPrintPreview will not have been invoked to set the last used
+ // printer name. For the reasons outlined at that hack, we want that set
+ // here too.
+ let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+ if (!PSSVC.lastUsedPrinterName) {
+ if (printSettings.printerName) {
+ PSSVC.maybeSaveLastUsedPrinterNameToPrefs(printSettings.printerName);
+ PSSVC.maybeSavePrintSettingsToPrefs(
+ printSettings,
+ Ci.nsIPrintSettings.kInitSaveAll
+ );
+ }
+ }
+ try {
+ var PRINTDIALOGSVC = Cc[
+ "@mozilla.org/widget/printdialog-service;1"
+ ].getService(Ci.nsIPrintDialogService);
+ PRINTDIALOGSVC.showPageSetupDialog(window, printSettings, null);
+ } catch (e) {
+ dump("showPageSetup " + e + "\n");
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * This call exists in a separate method so it can be easily overridden where
+ * `gBrowser` doesn't exist (e.g. Thunderbird).
+ *
+ * @see getTabDialogBox in tabbrowser.js
+ */
+ getTabDialogBox(sourceBrowser) {
+ return gBrowser.getTabDialogBox(sourceBrowser);
+ },
+
+ getPreviewBrowser(sourceBrowser) {
+ let dialogBox = this.getTabDialogBox(sourceBrowser);
+ for (let dialog of dialogBox.getTabDialogManager()._dialogs) {
+ let browser = dialog._box.querySelector(".printPreviewBrowser");
+ if (browser) {
+ return browser;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Opens the tab modal version of the print UI for the current tab.
+ *
+ * @param aBrowsingContext
+ * The BrowsingContext of the window to print.
+ * @param aExistingPreviewBrowser
+ * An existing browser created for printing from window.print().
+ * @param aPrintInitiationTime
+ * The time the print was initiated (typically by the user) as obtained
+ * from `Date.now()`. That is, the initiation time as the number of
+ * milliseconds since January 1, 1970.
+ * @param aPrintSelectionOnly
+ * Whether to print only the active selection of the given browsing
+ * context.
+ * @param aPrintFrameOnly
+ * Whether to print the selected frame only
+ * @return promise resolving when the dialog is open, rejected if the preview
+ * fails.
+ */
+ _openTabModalPrint(
+ aBrowsingContext,
+ aOpenWindowInfo,
+ aPrintInitiationTime,
+ aPrintSelectionOnly,
+ aPrintFrameOnly
+ ) {
+ let sourceBrowser = aBrowsingContext.top.embedderElement;
+ let previewBrowser = this.getPreviewBrowser(sourceBrowser);
+ if (previewBrowser) {
+ // Don't open another dialog if we're already printing.
+ //
+ // XXX This can be racy can't it? getPreviewBrowser looks at browser that
+ // we set up after opening the dialog. But I guess worst case we just
+ // open two dialogs so...
+ throw new Error("Tab-modal print UI already open");
+ }
+
+ // Create the print preview dialog.
+ let args = PromptUtils.objectToPropBag({
+ printSelectionOnly: !!aPrintSelectionOnly,
+ isArticle: sourceBrowser.isArticle,
+ printFrameOnly: !!aPrintFrameOnly,
+ });
+ let dialogBox = this.getTabDialogBox(sourceBrowser);
+ let { closedPromise, dialog } = dialogBox.open(
+ `chrome://global/content/print.html?printInitiationTime=${aPrintInitiationTime}`,
+ { features: "resizable=no", sizeTo: "available" },
+ args
+ );
+ closedPromise.catch(e => {
+ console.error(e);
+ });
+
+ let settingsBrowser = dialog._frame;
+ let printPreview = new PrintPreview({
+ sourceBrowsingContext: aBrowsingContext,
+ settingsBrowser,
+ topBrowsingContext: aBrowsingContext.top,
+ activeBrowsingContext: aBrowsingContext,
+ openWindowInfo: aOpenWindowInfo,
+ printFrameOnly: aPrintFrameOnly,
+ });
+ // This will create the source browser in connectedCallback() if we sent
+ // openWindowInfo. Otherwise the browser will be null.
+ settingsBrowser.parentElement.insertBefore(printPreview, settingsBrowser);
+ return printPreview.sourceBrowser;
+ },
+
+ /**
+ * Initialize a print, this will open the tab modal UI if it is enabled or
+ * defer to the native dialog/silent print.
+ *
+ * @param aBrowsingContext
+ * The BrowsingContext of the window to print.
+ * Note that the browsing context could belong to a subframe of the
+ * tab that called window.print, or similar shenanigans.
+ * @param aOptions
+ * {windowDotPrintOpenWindowInfo}
+ * Non-null if this call comes from window.print().
+ * This is the nsIOpenWindowInfo object that has to
+ * be passed down to createBrowser in order for the
+ * static clone that has been cretaed in the child
+ * process to be linked to the browser it creates
+ * in the parent process.
+ * {printSelectionOnly} Whether to print only the active selection of
+ * the given browsing context.
+ * {printFrameOnly} Whether to print the selected frame.
+ */
+ startPrintWindow(aBrowsingContext, aOptions) {
+ const printInitiationTime = Date.now();
+
+ // At most, one of these is set.
+ let { printSelectionOnly, printFrameOnly, windowDotPrintOpenWindowInfo } =
+ aOptions || {};
+
+ if (
+ windowDotPrintOpenWindowInfo &&
+ !windowDotPrintOpenWindowInfo.isForWindowDotPrint
+ ) {
+ throw new Error("Only expect openWindowInfo for window.print()");
+ }
+
+ let browsingContext = aBrowsingContext;
+ if (printSelectionOnly) {
+ // Ensure that we use the window with focus/selection if the context menu
+ // (from which 'Print selection' was selected) happens to have been opened
+ // over a different frame.
+ let focusedBc = Services.focus.focusedContentBrowsingContext;
+ if (
+ focusedBc &&
+ focusedBc.top.embedderElement == browsingContext.top.embedderElement
+ ) {
+ browsingContext = focusedBc;
+ }
+ }
+
+ if (!PRINT_ALWAYS_SILENT && !PREFER_SYSTEM_DIALOG) {
+ return this._openTabModalPrint(
+ browsingContext,
+ windowDotPrintOpenWindowInfo,
+ printInitiationTime,
+ printSelectionOnly,
+ printFrameOnly
+ );
+ }
+
+ const useSystemDialog = PREFER_SYSTEM_DIALOG && !PRINT_ALWAYS_SILENT;
+
+ let browser = null;
+ if (windowDotPrintOpenWindowInfo) {
+ // When we're called by handleStaticCloneCreatedForPrint(), we must
+ // return this browser.
+ browser = this.createParentBrowserForStaticClone(
+ browsingContext,
+ windowDotPrintOpenWindowInfo
+ );
+ browsingContext = browser.browsingContext;
+ }
+
+ // This code is wrapped in an async function so that we can await the async
+ // functions that it calls.
+ async function makePrintSettingsAndInvokePrint() {
+ let settings = PrintUtils.getPrintSettings(
+ /*aPrinterName*/ "",
+ /*aDefaultsOnly*/ false,
+ /*aAllowPseudoPrinter*/ !useSystemDialog
+ );
+ settings.printSelectionOnly = printSelectionOnly;
+ if (
+ settings.outputDestination ==
+ Ci.nsIPrintSettings.kOutputDestinationFile &&
+ !settings.toFileName
+ ) {
+ // TODO(bug 1748004): We should consider generating the file name
+ // from the document's title as we do in print.js's pickFileName
+ // (including using DownloadPaths.sanitize!).
+ // For now, the following is for consistency with the behavior
+ // prior to bug 1669149 part 3.
+ let dest = undefined;
+ try {
+ dest = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
+ } catch (e) {}
+ if (!dest) {
+ dest = Services.dirsvc.get("Home", Ci.nsIFile).path;
+ }
+ settings.toFileName = PathUtils.join(dest, "mozilla.pdf");
+ }
+
+ if (useSystemDialog) {
+ const hasSelection = await PrintUtils.checkForSelection(
+ browsingContext
+ );
+
+ // Prompt the user to choose a printer and make any desired print
+ // settings changes.
+ let doPrint = false;
+ try {
+ doPrint = await PrintUtils.handleSystemPrintDialog(
+ browsingContext.topChromeWindow,
+ hasSelection,
+ settings
+ );
+ if (!doPrint) {
+ return;
+ }
+ } finally {
+ // Clean up browser if we aren't going to use it.
+ if (!doPrint && browser) {
+ browser.remove();
+ }
+ }
+ }
+
+ // At some point we should handle the Promise that this returns (at
+ // least report rejection to telemetry).
+ browsingContext.print(settings);
+ }
+
+ // We need to return to the event loop before calling
+ // makePrintSettingsAndInvokePrint() if we were called for `window.print()`.
+ // That's because if that function synchronously calls `browser.remove()`
+ // or `browsingContext.print()` before we return `browser`, the nested
+ // event loop that is being spun in the content process under the
+ // OpenInternal call in nsGlobalWindowOuter::Print will still be active.
+ // In the case of `browser.remove()`, nsGlobalWindowOuter::Print would then
+ // get unhappy once OpenInternal does return since it will fail to return
+ // a BrowsingContext. In the case of `browsingContext.print()`, we would
+ // re-enter nsGlobalWindowOuter::Print under the nested event loop and
+ // printing would then fail since the outer nsGlobalWindowOuter::Print call
+ // wouldn't yet have created the static clone.
+ setTimeout(makePrintSettingsAndInvokePrint, 0);
+
+ return browser;
+ },
+
+ togglePrintPreview(aBrowsingContext) {
+ let dialogBox = this.getTabDialogBox(aBrowsingContext.top.embedderElement);
+ let dialogs = dialogBox.getTabDialogManager().dialogs;
+ let previewDialog = dialogs.find(d =>
+ d._box.querySelector(".printSettingsBrowser")
+ );
+ if (previewDialog) {
+ previewDialog.close();
+ return;
+ }
+ this.startPrintWindow(aBrowsingContext);
+ },
+
+ /**
+ * Called when a content process has created a new BrowsingContext for a
+ * static clone of a document that is to be printed, but we do NOT yet have a
+ * CanonicalBrowsingContext counterpart in the parent process. This only
+ * happens in the following cases:
+ *
+ * - content script invoked window.print() in the content process, or:
+ * - silent printing is enabled, and UI code previously invoked
+ * startPrintWindow which called BrowsingContext.print(), and we're now
+ * being called back by the content process to parent the static clone.
+ *
+ * In the latter case we only need to create the CanonicalBrowsingContext,
+ * link it to it's content process counterpart, and inserted it into
+ * the document tree; the print in the content process has already been
+ * initiated.
+ *
+ * In the former case we additionally need to check if we should open the
+ * tab modal print UI (if not silent printing), obtain a valid
+ * nsIPrintSettings object, and tell the content process to initiate the
+ * print with this settings object.
+ */
+ handleStaticCloneCreatedForPrint(aOpenWindowInfo) {
+ let browsingContext = aOpenWindowInfo.parent;
+ if (aOpenWindowInfo.isForWindowDotPrint) {
+ return this.startPrintWindow(browsingContext, {
+ windowDotPrintOpenWindowInfo: aOpenWindowInfo,
+ });
+ }
+ return this.createParentBrowserForStaticClone(
+ browsingContext,
+ aOpenWindowInfo
+ );
+ },
+
+ createParentBrowserForStaticClone(aBrowsingContext, aOpenWindowInfo) {
+ // XXX This code is only called when silent printing, so we're really
+ // abusing PrintPreview here. See bug 1768020.
+ let printPreview = new PrintPreview({
+ sourceBrowsingContext: aBrowsingContext,
+ openWindowInfo: aOpenWindowInfo,
+ });
+ let browser = printPreview.createPreviewBrowser("source");
+ document.documentElement.append(browser);
+ return browser;
+ },
+
+ // "private" methods and members. Don't use them.
+
+ _getErrorCodeForNSResult(nsresult) {
+ const MSG_CODES = [
+ "GFX_PRINTER_NO_PRINTER_AVAILABLE",
+ "GFX_PRINTER_NAME_NOT_FOUND",
+ "GFX_PRINTER_COULD_NOT_OPEN_FILE",
+ "GFX_PRINTER_STARTDOC",
+ "GFX_PRINTER_ENDDOC",
+ "GFX_PRINTER_STARTPAGE",
+ "GFX_PRINTER_DOC_IS_BUSY",
+ "ABORT",
+ "NOT_AVAILABLE",
+ "NOT_IMPLEMENTED",
+ "OUT_OF_MEMORY",
+ "UNEXPECTED",
+ ];
+
+ for (let code of MSG_CODES) {
+ let nsErrorResult = "NS_ERROR_" + code;
+ if (Cr[nsErrorResult] == nsresult) {
+ return code;
+ }
+ }
+
+ // PERR_FAILURE is the catch-all error message if we've gotten one that
+ // we don't recognize.
+ return "FAILURE";
+ },
+
+ _displayPrintingError(nsresult, isPrinting, browser) {
+ // The nsresults from a printing error are mapped to strings that have
+ // similar names to the errors themselves. For example, for error
+ // NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE, the name of the string
+ // for the error message is: PERR_GFX_PRINTER_NO_PRINTER_AVAILABLE. What's
+ // more, if we're in the process of doing a print preview, it's possible
+ // that there are strings specific for print preview for these errors -
+ // if so, the names of those strings have _PP as a suffix. It's possible
+ // that no print preview specific strings exist, in which case it is fine
+ // to fall back to the original string name.
+ let msgName = "PERR_" + this._getErrorCodeForNSResult(nsresult);
+ let msg, title;
+ if (!isPrinting) {
+ // Try first with _PP suffix.
+ let ppMsgName = msgName + "_PP";
+ try {
+ msg = this._bundle.GetStringFromName(ppMsgName);
+ } catch (e) {
+ // We allow localizers to not have the print preview error string,
+ // and just fall back to the printing error string.
+ }
+ }
+
+ if (!msg) {
+ msg = this._bundle.GetStringFromName(msgName);
+ }
+
+ title = this._bundle.GetStringFromName(
+ isPrinting
+ ? "print_error_dialog_title"
+ : "printpreview_error_dialog_title"
+ );
+
+ Services.prompt.asyncAlert(
+ browser.browsingContext,
+ Services.prompt.MODAL_TYPE_TAB,
+ title,
+ msg
+ );
+
+ Services.telemetry.keyedScalarAdd(
+ "printing.error",
+ this._getErrorCodeForNSResult(nsresult),
+ 1
+ );
+ },
+
+ getPrintSettings(aPrinterName, aDefaultsOnly, aAllowPseudoPrinter = true) {
+ var printSettings;
+ try {
+ var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+
+ function isValidPrinterName(aPrinterName) {
+ return (
+ aPrinterName &&
+ (aAllowPseudoPrinter ||
+ aPrinterName != PrintUtils.SAVE_TO_PDF_PRINTER)
+ );
+ }
+
+ // We must not try to print using an nsIPrintSettings without a printer
+ // name set.
+ const printerName = (function () {
+ if (isValidPrinterName(aPrinterName)) {
+ return aPrinterName;
+ }
+ if (isValidPrinterName(PSSVC.lastUsedPrinterName)) {
+ return PSSVC.lastUsedPrinterName;
+ }
+ return Cc["@mozilla.org/gfx/printerlist;1"].getService(
+ Ci.nsIPrinterList
+ ).systemDefaultPrinterName;
+ })();
+
+ printSettings = PSSVC.createNewPrintSettings();
+ printSettings.printerName = printerName;
+
+ // First get any defaults from the printer. We want to skip this for Save
+ // to PDF since it isn't a real printer and will throw.
+ if (printSettings.printerName != this.SAVE_TO_PDF_PRINTER) {
+ PSSVC.initPrintSettingsFromPrinter(
+ printSettings.printerName,
+ printSettings
+ );
+ }
+
+ if (!aDefaultsOnly) {
+ // Apply any settings that have been saved for this printer.
+ PSSVC.initPrintSettingsFromPrefs(
+ printSettings,
+ true,
+ printSettings.kInitSaveAll
+ );
+ }
+ } catch (e) {
+ console.error("PrintUtils.getPrintSettings failed: ", e, "\n");
+ }
+ return printSettings;
+ },
+
+ // Show the system print dialog, saving modified preferences.
+ // Returns true if the user clicked print (Not cancel).
+ async handleSystemPrintDialog(aWindow, aHasSelection, aSettings) {
+ // Prompt the user to choose a printer and make any desired print
+ // settings changes.
+ try {
+ const svc = Cc["@mozilla.org/widget/printdialog-service;1"].getService(
+ Ci.nsIPrintDialogService
+ );
+ await svc.showPrintDialog(aWindow, aHasSelection, aSettings);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ return false;
+ }
+ throw e;
+ }
+
+ // Update the saved last used printer name and print settings:
+ var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+ PSSVC.maybeSaveLastUsedPrinterNameToPrefs(aSettings.printerName);
+ PSSVC.maybeSavePrintSettingsToPrefs(
+ aSettings,
+ Ci.nsIPrintSettings.kPrintDialogPersistSettings
+ );
+ return true;
+ },
+};
+
+class PrintPreview extends MozElements.BaseControl {
+ constructor({
+ sourceBrowsingContext,
+ settingsBrowser,
+ topBrowsingContext,
+ activeBrowsingContext,
+ openWindowInfo,
+ printFrameOnly,
+ }) {
+ super();
+ this.sourceBrowsingContext = sourceBrowsingContext;
+ this.settingsBrowser = settingsBrowser;
+ this.topBrowsingContext = topBrowsingContext;
+ this.activeBrowsingContext = activeBrowsingContext;
+ this.openWindowInfo = openWindowInfo;
+ this.printFrameOnly = printFrameOnly;
+
+ this.printSelectionOnly = false;
+ this.simplifyPage = false;
+ this.sourceBrowser = null;
+ this.selectionBrowser = null;
+ this.simplifiedBrowser = null;
+ this.lastPreviewBrowser = null;
+ }
+
+ connectedCallback() {
+ if (this.childElementCount > 0) {
+ return;
+ }
+ this.setAttribute("flex", "1");
+ this.append(
+ MozXULElement.parseXULToFragment(`
+ <stack class="previewStack" rendering="true" flex="1" previewtype="primary">
+ <vbox class="previewRendering" flex="1">
+ <h1 class="print-pending-label" data-l10n-id="printui-loading"></h1>
+ </vbox>
+ <html:printpreview-pagination class="printPreviewNavigation"></html:printpreview-pagination>
+ </stack>
+ `)
+ );
+ this.stack = this.firstElementChild;
+ this.paginator = this.querySelector("printpreview-pagination");
+
+ if (this.openWindowInfo) {
+ // For window.print() we need a browser right away for the contents to be
+ // cloned into, create it now.
+ this.createPreviewBrowser("source");
+ }
+ }
+
+ disconnectedCallback() {
+ this.exitPrintPreview();
+ }
+
+ getSourceBrowsingContext() {
+ if (this.openWindowInfo) {
+ // If openWindowInfo is set this was for window.print() and the source
+ // contents have already been cloned into the preview browser.
+ return this.sourceBrowser.browsingContext;
+ }
+ return this.sourceBrowsingContext;
+ }
+
+ get currentBrowsingContext() {
+ return this.lastPreviewBrowser.browsingContext;
+ }
+
+ exitPrintPreview() {
+ this.sourceBrowser?.frameLoader?.exitPrintPreview();
+ this.simplifiedBrowser?.frameLoader?.exitPrintPreview();
+ this.selectionBrowser?.frameLoader?.exitPrintPreview();
+
+ this.textContent = "";
+ }
+
+ async printPreview(settings, { sourceVersion, sourceURI }) {
+ this.stack.setAttribute("rendering", true);
+
+ let result = await this._printPreview(settings, {
+ sourceVersion,
+ sourceURI,
+ });
+
+ let browser = this.lastPreviewBrowser;
+ this.stack.setAttribute("previewtype", browser.getAttribute("previewtype"));
+ browser.setAttribute("sheet-count", result.sheetCount);
+ // The view resets to the top of the document on update bug 1686737.
+ browser.setAttribute("current-page", 1);
+ this.paginator.observePreviewBrowser(browser);
+ await document.l10n.translateElements([browser]);
+
+ this.stack.removeAttribute("rendering");
+
+ return result;
+ }
+
+ async _printPreview(settings, { sourceVersion, sourceURI }) {
+ let printSelectionOnly = sourceVersion == "selection";
+ let simplifyPage = sourceVersion == "simplified";
+ let selectionTypeBrowser;
+ let previewBrowser;
+
+ // Select the existing preview browser elements, these could be null.
+ if (printSelectionOnly) {
+ selectionTypeBrowser = this.selectionBrowser;
+ previewBrowser = this.selectionBrowser;
+ } else {
+ selectionTypeBrowser = this.sourceBrowser;
+ previewBrowser = simplifyPage
+ ? this.simplifiedBrowser
+ : this.sourceBrowser;
+ }
+
+ settings.docURL = sourceURI;
+
+ if (previewBrowser) {
+ this.lastPreviewBrowser = previewBrowser;
+ if (this.openWindowInfo) {
+ // We only want to use openWindowInfo for the window.print() browser,
+ // we can get rid of it now.
+ this.openWindowInfo = null;
+ }
+ // This browser has been rendered already, just update it.
+ return previewBrowser.frameLoader.printPreview(settings, null);
+ }
+
+ if (!selectionTypeBrowser) {
+ // Need to create a non-simplified browser.
+ selectionTypeBrowser = this.createPreviewBrowser(
+ simplifyPage ? "source" : sourceVersion
+ );
+ let browsingContext =
+ printSelectionOnly || this.printFrameOnly
+ ? this.activeBrowsingContext
+ : this.topBrowsingContext;
+ let result = await selectionTypeBrowser.frameLoader.printPreview(
+ settings,
+ browsingContext
+ );
+ // If this isn't simplified then we're done.
+ if (!simplifyPage) {
+ this.lastPreviewBrowser = selectionTypeBrowser;
+ return result;
+ }
+ }
+
+ // We have the base selection/primary browser but need to simplify.
+ previewBrowser = this.createPreviewBrowser(sourceVersion);
+ await previewBrowser.browsingContext.currentWindowGlobal
+ .getActor("Printing")
+ .sendQuery("Printing:Preview:ParseDocument", {
+ URL: sourceURI,
+ windowID:
+ selectionTypeBrowser.browsingContext.currentWindowGlobal
+ .outerWindowId,
+ });
+
+ // We've parsed a simplified version into the preview browser. Convert that to
+ // a print preview as usual.
+ this.lastPreviewBrowser = previewBrowser;
+ return previewBrowser.frameLoader.printPreview(
+ settings,
+ previewBrowser.browsingContext
+ );
+ }
+
+ createPreviewBrowser(sourceVersion) {
+ let browser = document.createXULElement("browser");
+ let browsingContext =
+ sourceVersion == "selection" ||
+ this.printFrameOnly ||
+ (sourceVersion == "source" && this.openWindowInfo)
+ ? this.sourceBrowsingContext
+ : this.sourceBrowsingContext.top;
+ if (sourceVersion == "source" && this.openWindowInfo) {
+ browser.openWindowInfo = this.openWindowInfo;
+ } else {
+ let userContextId = browsingContext.originAttributes.userContextId;
+ if (userContextId) {
+ browser.setAttribute("usercontextid", userContextId);
+ }
+ browser.setAttribute(
+ "initialBrowsingContextGroupId",
+ browsingContext.group.id
+ );
+ }
+ browser.setAttribute("type", "content");
+ let remoteType = browsingContext.currentRemoteType;
+ if (remoteType) {
+ browser.setAttribute("remoteType", remoteType);
+ browser.setAttribute("remote", "true");
+ }
+ // When the print process finishes, we get closed by
+ // nsDocumentViewer::OnDonePrinting, or by the print preview code.
+ //
+ // When that happens, we should remove us from the DOM if connected.
+ browser.addEventListener("DOMWindowClose", function (e) {
+ if (this.isConnected) {
+ this.remove();
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ });
+
+ if (this.settingsBrowser) {
+ browser.addEventListener("contextmenu", function (e) {
+ e.preventDefault();
+ });
+
+ browser.setAttribute("previewtype", sourceVersion);
+ browser.classList.add("printPreviewBrowser");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("printpreview", "true");
+ browser.setAttribute("nodefaultsrc", "true");
+ document.l10n.setAttributes(browser, "printui-preview-label");
+
+ this.stack.insertBefore(browser, this.paginator);
+
+ if (sourceVersion == "source") {
+ this.sourceBrowser = browser;
+ } else if (sourceVersion == "selection") {
+ this.selectionBrowser = browser;
+ } else if (sourceVersion == "simplified") {
+ this.simplifiedBrowser = browser;
+ }
+ } else {
+ browser.style.visibility = "collapse";
+ }
+ return browser;
+ }
+}
+customElements.define("print-preview", PrintPreview);
diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css
new file mode 100644
index 0000000000..71e5d8fc1c
--- /dev/null
+++ b/toolkit/components/printing/content/simplifyMode.css
@@ -0,0 +1,36 @@
+/* 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/. */
+
+/* This file defines specific rules for print preview when using simplify mode.
+ * Some of these rules (styling for title and author on the header element)
+ * already exist in aboutReader.css, however, we decoupled it from the original
+ * file so we don't need to load a bunch of extra queries that will not take
+ * effect when using the simplify page checkbox. */
+:root {
+ --font-size: 16px;
+ --content-width: 30em;
+ --line-height: 1.6em;
+}
+
+body {
+ padding: 0px;
+}
+
+.container {
+ max-width: 100%;
+ font-family: Georgia, "Times New Roman", serif;
+}
+
+.header > h1 {
+ font-size: 1.6em;
+ line-height: 1.25em;
+ margin: 30px 0;
+}
+
+.header > .credits {
+ font-size: 0.9em;
+ line-height: 1.48em;
+ margin: 0 0 30px 0;
+ font-style: italic;
+}
diff --git a/toolkit/components/printing/content/toggle-group.css b/toolkit/components/printing/content/toggle-group.css
new file mode 100644
index 0000000000..94922eb963
--- /dev/null
+++ b/toolkit/components/printing/content/toggle-group.css
@@ -0,0 +1,79 @@
+/* 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/. */
+
+
+/*
+ * A radiogroup styled to hide the radio buttons
+ * and present tab-like labels as buttons
+ */
+
+.toggle-group {
+ display: inline-flex;
+}
+
+.toggle-group-label {
+ display: inline-flex;
+ align-items: center;
+ margin: 0;
+ padding: 6px 10px;
+ background-color: var(--in-content-button-background);
+}
+
+.toggle-group-input {
+ position: absolute;
+ inset-inline-start: -100px;
+}
+
+.toggle-group-label-iconic::before {
+ width: 16px;
+ height: 16px;
+ margin-inline-end: 5px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.toggle-group-label:first-of-type {
+ border-start-start-radius: 4px;
+ border-end-start-radius: 4px;
+}
+
+.toggle-group-label:last-of-type {
+ border-start-end-radius: 4px;
+ border-end-end-radius: 4px;
+}
+
+.toggle-group-input:disabled + .toggle-group-label {
+ opacity: 0.4;
+}
+
+.toggle-group-input:enabled + .toggle-group-label:hover {
+ background-color: var(--in-content-button-background-hover);
+ color: var(--in-content-button-text-color-hover);
+}
+
+.toggle-group-input:enabled + .toggle-group-label:hover:active {
+ background-color: var(--in-content-button-background-active);
+ color: var(--in-content-button-text-color-active);
+}
+
+.toggle-group-input:focus-visible + .toggle-group-label {
+ outline: var(--in-content-focus-outline);
+ outline-offset: var(--in-content-focus-outline-offset);
+ z-index: 1;
+}
+
+.toggle-group-input:checked + .toggle-group-label {
+ background-color: var(--in-content-primary-button-background);
+ color: var(--in-content-primary-button-text-color);
+}
+
+.toggle-group-input:enabled:checked + .toggle-group-label:hover {
+ background-color: var(--in-content-primary-button-background-hover);
+ color: var(--in-content-primary-button-text-color-hover);
+}
+
+.toggle-group-input:enabled:checked + .toggle-group-label:hover:active {
+ background-color: var(--in-content-primary-button-background-active);
+ color: var(--in-content-primary-button-text-color-active);
+}