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/landscape.svg8
-rw-r--r--toolkit/components/printing/content/portrait.svg8
-rw-r--r--toolkit/components/printing/content/print.css349
-rw-r--r--toolkit/components/printing/content/print.html228
-rw-r--r--toolkit/components/printing/content/print.js2562
-rw-r--r--toolkit/components/printing/content/printPageSetup.js540
-rw-r--r--toolkit/components/printing/content/printPageSetup.xhtml212
-rw-r--r--toolkit/components/printing/content/printPagination.css143
-rw-r--r--toolkit/components/printing/content/printPreviewPagination.js208
-rw-r--r--toolkit/components/printing/content/printPreviewProgress.js152
-rw-r--r--toolkit/components/printing/content/printPreviewProgress.xhtml34
-rw-r--r--toolkit/components/printing/content/printPreviewToolbar.js444
-rw-r--r--toolkit/components/printing/content/printProgress.js230
-rw-r--r--toolkit/components/printing/content/printProgress.xhtml47
-rw-r--r--toolkit/components/printing/content/printUtils.js1073
-rw-r--r--toolkit/components/printing/content/simplifyMode.css32
-rw-r--r--toolkit/components/printing/content/toggle-group.css79
17 files changed, 6349 insertions, 0 deletions
diff --git a/toolkit/components/printing/content/landscape.svg b/toolkit/components/printing/content/landscape.svg
new file mode 100644
index 0000000000..5e6bbd9e02
--- /dev/null
+++ b/toolkit/components/printing/content/landscape.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M11.5 2.5l4 4v6c0 0.6-0.4 1-1 1h-13c-0.6 0-1-0.4-1-1v-9c0-0.6 0.4-1 1-1h10z" fill-opacity=".1"/>
+ <path d="M15.5 6.5h-4v-4l4 4z" fill-opacity=".3"/>
+ <path d="M16 6.4c0-0.1-0.1-0.2-0.1-0.3l-4-4c-0.1 0-0.2-0.1-0.3-0.1H1.5C0.7 2 0 2.7 0 3.5v9C0 13.3 0.7 14 1.5 14h13c0.8 0 1.5-0.7 1.5-1.5V6.4zM14.3 6H12V3.7L14.3 6zm0.2 7h-13C1.2 13 1 12.8 1 12.5v-9C1 3.2 1.2 3 1.5 3H11v3.5C11 6.8 11.2 7 11.5 7H15v5.5c0 0.3-0.2 0.5-0.5 0.5z"/>
+</svg>
diff --git a/toolkit/components/printing/content/portrait.svg b/toolkit/components/printing/content/portrait.svg
new file mode 100644
index 0000000000..f520820efb
--- /dev/null
+++ b/toolkit/components/printing/content/portrait.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity">
+ <path d="M13.5 4.5l-4-4h-6c-0.6 0-1 0.4-1 1v13c0 0.6 0.4 1 1 1h9c0.6 0 1-0.4 1-1v-10z" fill-opacity=".1"/>
+ <path d="M9.5 0.5v4h4l-4-4z" fill-opacity=".3"/>
+ <path d="M14 4.4c0-0.1-0.1-0.2-0.1-0.3l-4-4C9.8 0.1 9.7 0 9.6 0H3.5C2.7 0 2 0.7 2 1.5v13C2 15.3 2.7 16 3.5 16h9c0.8 0 1.5-0.7 1.5-1.5V4.4zM12.3 4H10V1.7L12.3 4zm0.2 11h-9C3.2 15 3 14.8 3 14.5v-13C3 1.2 3.2 1 3.5 1H9v3.5C9 4.8 9.2 5 9.5 5H13v9.5c0 0.3-0.2 0.5-0.5 0.5z"/>
+</svg>
diff --git a/toolkit/components/printing/content/print.css b/toolkit/components/printing/content/print.css
new file mode 100644
index 0000000000..0c2b0f10e3
--- /dev/null
+++ b/toolkit/components/printing/content/print.css
@@ -0,0 +1,349 @@
+/* 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 {
+ width: 250px;
+ height: 100vh;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow: hidden;
+ background: var(--in-content-box-background);
+}
+
+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 > input,
+.row > select,
+.col > input {
+ margin-inline: 0 0;
+ max-width: 100%;
+ display: inline-block;
+}
+
+.row.cols-2 {
+ flex-direction: row;
+ align-items: center;
+}
+
+.cols-2 > .col:nth-child(1) {
+ flex: 0 0 2em;
+ text-align: center;
+}
+
+.header-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ flex: 0 1 auto;
+ border-bottom: 1px solid var(--in-content-border-color);
+ 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 {
+ min-height: auto;
+ margin: 0;
+ padding: 0;
+}
+
+select:not([size], [multiple])[iconic] {
+ padding-inline-start: 28px;
+}
+
+#printer-picker {
+ background-size: auto 12px, 16px;
+ background-image: url("chrome://global/skin/icons/arrow-dropdown-12.svg"), url("chrome://global/skin/icons/print.svg");
+ background-position: right 3px center, left 8px center;
+ width: 100%;
+}
+
+#printer-picker:dir(rtl) {
+ background-position-x: left 3px, right 8px;
+}
+
+#printer-picker[output="pdf"] {
+ background-image: url("chrome://global/skin/icons/arrow-dropdown-12.svg"), url("chrome://global/content/portrait.svg");
+}
+
+input[type="checkbox"] {
+ margin-inline-end: 8px;
+}
+
+input[type="radio"] {
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: 50%;
+ margin: 0;
+ margin-inline-end: 8px;
+ background-color: var(--in-content-box-background);
+}
+
+input[type="radio"]:checked {
+ appearance: none;
+ background-image: url("chrome://global/skin/in-content/radio.svg");
+ -moz-context-properties: fill;
+ fill: #3485ff;
+}
+
+input[type="radio"]:enabled:-moz-focusring,
+input[type="radio"]:enabled:hover {
+ border-color: var(--in-content-border-focus);
+}
+
+input[type="radio"]:enabled:-moz-focusring {
+ outline: 2px solid var(--in-content-border-active);
+ /* offset outline to align with 1px border-width set for buttons/menulists above. */
+ outline-offset: -1px;
+ /* Make outline-radius slightly bigger than the border-radius set above,
+ * to make the thicker outline corners look smooth */
+ -moz-outline-radius: 50%;
+ box-shadow: 0 0 0 4px var(--in-content-border-active-shadow);
+}
+
+input[type="radio"]:disabled {
+ opacity: 0.5;
+}
+
+input[type="number"],
+input[type="text"] {
+ padding: 2px;
+ padding-inline-start: 4px;
+ outline: none !important;
+}
+
+.cols-2 > input {
+ flex: none;
+}
+
+.toggle-group-label {
+ padding: 4px 8px;
+}
+
+.body-container {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+#more-settings {
+ border-top: 1px solid var(--in-content-border-color);
+}
+
+.twisty > summary {
+ list-style: none;
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+}
+
+#more-settings > summary > .twisty {
+ background-image: url("chrome://global/skin/icons/twisty-expanded.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 12px;
+ height: 12px;
+ scale: 1 1;
+}
+
+#more-settings > summary > .label {
+ flex-grow: 1;
+}
+
+#more-settings[open] > summary > .twisty {
+ /* flip arrow to point up for the open state */
+ scale: 1 -1;
+}
+
+#open-dialog-link {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#open-dialog-link::after {
+ display: block;
+ content: url(chrome://global/skin/icons/open-in-new.svg);
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 12px;
+ height: 12px;
+}
+
+#open-dialog-link:dir(rtl)::after {
+ scale: -1 1;
+}
+
+.footer-container {
+ border-top: 1px solid var(--in-content-border-color);
+ 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;
+}
+
+#button-container {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+}
+
+#button-container > button {
+ flex: 1 1 auto;
+ margin: 0;
+}
+
+#custom-range {
+ margin-top: 4px;
+}
+
+.vertical-margins,
+.horizontal-margins {
+ display: flex;
+ gap: 20px;
+ margin-block: 5px;
+}
+
+.margin-input {
+ width: 6em !important;
+}
+
+.margin-descriptor {
+ text-align: center;
+ display: block;
+ margin-top: 2px;
+}
+
+.toggle-group #landscape + .toggle-group-label::before {
+ content: url("chrome://global/content/landscape.svg");
+}
+.toggle-group #portrait + .toggle-group-label::before {
+ content: url("chrome://global/content/portrait.svg");
+}
+
+select:invalid,
+input[type="text"]:invalid,
+input[type="number"]:invalid {
+ border: 1px solid #D70022;
+ box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3);
+}
+
+.error-message {
+ font-size: 12px;
+ color: #D70022;
+ margin-top: 4px;
+}
+
+#percent-scale {
+ margin-inline-start: 4px;
+}
+
+input[type="number"].photon-number {
+ padding: 0;
+ padding-inline-start: 4px;
+ margin: 0;
+ height: 20px;
+ 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;
+}
+
+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-dropdown-16.svg");
+ background-size: 8px;
+ background-position: center;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+input[type="number"].photon-number::-moz-number-spin-up {
+ scale: 1 -1;
+}
+
+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);
+}
diff --git a/toolkit/components/printing/content/print.html b/toolkit/components/printing/content/print.html
new file mode 100644
index 0000000000..7394069efe
--- /dev/null
+++ b/toolkit/components/printing/content/print.html
@@ -0,0 +1,228 @@
+<!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>
+ </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="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">
+ <div class="row cols-2">
+ <input type="radio" name="scale-choice" id="fit-choice" value="fit" checked>
+ <label for="fit-choice" data-l10n-id="printui-scale-fit-to-page-width" class="col"></label>
+ </div>
+ <div class="row cols-2">
+ <input type="radio" name="scale-choice" id="percent-scale-choice">
+ <span class="col">
+ <label id="percent-scale-label" for="percent-scale-choice" data-l10n-id="printui-scale-pcent"></label>
+ <!-- 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>
+ </div>
+ <p id="error-invalid-scale" hidden data-l10n-id="printui-error-invalid-scale" class="error-message" role="alert"></p>
+ </div>
+ </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-l10n-id="printui-margins-custom-inches"></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-l10n-id="printui-margins-custom-top-inches"></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-l10n-id="printui-margins-custom-bottom-inches"></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-l10n-id="printui-margins-custom-left-inches"></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-l10n-id="printui-margins-custom-right-inches"></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>
+
+ <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>
+ <input id="copies-count" is="setting-number" data-setting-name="numCopies" min="1" max="10000" class="copy-count-input photon-number" required>
+ </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>
+
+ <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" hidden>
+ <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" data-l10n-id="printui-two-sided-printing"></label>
+ <div class="row cols-2">
+ <input is="setting-checkbox" id="duplex-enabled" data-setting-name="printDuplex">
+ <label for="duplex-enabled" data-l10n-id="printui-duplex-checkbox"></label>
+ </div>
+ </section>
+
+ <section id="more-settings-options" class="section-block">
+ <label class="block-label" data-l10n-id="printui-options"></label>
+ <div id="headers-footers" class="row cols-2">
+ <input is="setting-checkbox" id="headers-footers-enabled" data-setting-name="printFootersHeaders">
+ <label for="headers-footers-enabled" data-l10n-id="printui-headers-footers-checkbox"></label>
+ </div>
+ <div id="backgrounds" class="row cols-2">
+ <input is="setting-checkbox" id="backgrounds-enabled" data-setting-name="printBackgrounds">
+ <label for="backgrounds-enabled" data-l10n-id="printui-backgrounds-checkbox"></label>
+ </div>
+ <div id="print-selection-container" class="row cols-2" hidden>
+ <input is="setting-checkbox" id="print-selection-enabled" data-setting-name="printSelectionOnly">
+ <label for="print-selection-enabled" data-l10n-id="printui-selection-checkbox"></label>
+ </div>
+ </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>
+
+ <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>
+ <section 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>
+ </section>
+ </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..826893a0ec
--- /dev/null
+++ b/toolkit/components/printing/content/print.js
@@ -0,0 +1,2562 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {
+ gBrowser,
+ PrintUtils,
+ Services,
+ AppConstants,
+} = window.docShell.chromeEventHandler.ownerGlobal;
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm"
+);
+
+const 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 ourBrowser = window.docShell.chromeEventHandler;
+const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+);
+
+var logger = (function() {
+ const getMaxLogLevel = () =>
+ Services.prefs.getBoolPref("print.debug", false) ? "all" : "warn";
+
+ let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+ // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+ let _logger = new ConsoleAPI({
+ prefix: "printUI",
+ maxLogLevel: getMaxLogLevel(),
+ });
+
+ function onPrefChange() {
+ if (_logger) {
+ _logger.maxLogLevel = getMaxLogLevel();
+ }
+ }
+ // Watch for pref changes and the maxLogLevel for the logger
+ Services.prefs.addObserver("print.debug", onPrefChange);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("print.debug", onPrefChange);
+ });
+ return _logger;
+})();
+
+function serializeSettings(settings, logPrefix) {
+ let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception?
+ let types = new Set(["string", "boolean", "number", "undefined"]);
+ let nameValues = {};
+ for (let key in settings) {
+ try {
+ if (!re.test(key) && types.has(typeof settings[key])) {
+ nameValues[key] = settings[key];
+ }
+ } catch (e) {
+ logger.warn("Exception accessing setting: ", key, e);
+ }
+ }
+ return nameValues;
+}
+
+let printPending = false;
+let deferredTasks = [];
+function createDeferredTask(fn, timeout) {
+ let task = new DeferredTask(fn, timeout);
+ deferredTasks.push(task);
+ return task;
+}
+
+function cancelDeferredTasks() {
+ for (let task of deferredTasks) {
+ task.disarm();
+ }
+ PrintEventHandler._updatePrintPreviewTask?.disarm();
+ deferredTasks = [];
+}
+
+document.addEventListener(
+ "DOMContentLoaded",
+ e => {
+ window._initialized = PrintEventHandler.init();
+ ourBrowser.setAttribute("flex", "0");
+ ourBrowser.classList.add("printSettingsBrowser");
+ ourBrowser.closest(".dialogBox")?.classList.add("printDialogBox");
+ },
+ { once: true }
+);
+
+window.addEventListener("dialogclosing", () => {
+ PrintEventHandler.unload();
+ cancelDeferredTasks();
+});
+
+window.addEventListener(
+ "unload",
+ e => {
+ document.textContent = "";
+ },
+ { once: true }
+);
+
+var PrintEventHandler = {
+ settings: null,
+ defaultSettings: null,
+ allPaperSizes: {},
+ previewIsEmpty: false,
+ _delayedChanges: {},
+ _hasRenderedSelectionPreview: false,
+ _hasRenderedPrimaryPreview: false,
+ _userChangedSettings: {},
+ settingFlags: {
+ margins: Ci.nsIPrintSettings.kInitSaveMargins,
+ customMargins: Ci.nsIPrintSettings.kInitSaveMargins,
+ orientation: Ci.nsIPrintSettings.kInitSaveOrientation,
+ paperId:
+ Ci.nsIPrintSettings.kInitSavePaperSize |
+ Ci.nsIPrintSettings.kInitSaveUnwriteableMargins,
+ printInColor: Ci.nsIPrintSettings.kInitSaveInColor,
+ scaling: Ci.nsIPrintSettings.kInitSaveScaling,
+ shrinkToFit: Ci.nsIPrintSettings.kInitSaveShrinkToFit,
+ printDuplex: Ci.nsIPrintSettings.kInitSaveDuplex,
+ printFootersHeaders:
+ Ci.nsIPrintSettings.kInitSaveHeaderLeft |
+ Ci.nsIPrintSettings.kInitSaveHeaderCenter |
+ Ci.nsIPrintSettings.kInitSaveHeaderRight |
+ Ci.nsIPrintSettings.kInitSaveFooterLeft |
+ Ci.nsIPrintSettings.kInitSaveFooterCenter |
+ Ci.nsIPrintSettings.kInitSaveFooterRight,
+ printBackgrounds:
+ Ci.nsIPrintSettings.kInitSaveBGColors |
+ Ci.nsIPrintSettings.kInitSaveBGImages,
+ },
+ originalSourceContentTitle: null,
+ originalSourceCurrentURI: null,
+ previewBrowser: null,
+ selectionPreviewBrowser: null,
+ currentPreviewBrowser: null,
+
+ // These settings do not have an associated pref value or flag, but
+ // changing them requires us to update the print preview.
+ _nonFlaggedUpdatePreviewSettings: new Set([
+ "pageRanges",
+ "numPagesPerSheet",
+ "printSelectionOnly",
+ ]),
+ _noPreviewUpdateSettings: new Set(["numCopies", "printDuplex"]),
+
+ async init() {
+ Services.telemetry.scalarAdd("printing.preview_opened_tm", 1);
+
+ // Do not keep a reference to source browser, it may mutate after printing
+ // is initiated and the print preview clone must be a snapshot from the
+ // time that the print was started.
+ let sourceBrowsingContext = this.getSourceBrowsingContext();
+ ({
+ previewBrowser: this.previewBrowser,
+ selectionPreviewBrowser: this.selectionPreviewBrowser,
+ } = PrintUtils.createPreviewBrowsers(sourceBrowsingContext, ourBrowser));
+
+ let args = window.arguments[0];
+ this.printSelectionOnly = args.getProperty("printSelectionOnly");
+ this.hasSelection =
+ args.getProperty("hasSelection") && this.selectionPreviewBrowser;
+ this.printFrameOnly = args.getProperty("printFrameOnly");
+ // Get the temporary browser that will previously have been created for the
+ // platform code to generate the static clone printing doc into if this
+ // print is for a window.print() call. In that case we steal the browser's
+ // docshell to get the static clone, then discard it.
+ let existingBrowser = args.getProperty("previewBrowser");
+ if (existingBrowser) {
+ sourceBrowsingContext = existingBrowser.browsingContext;
+ this.previewBrowser.swapDocShells(existingBrowser);
+ existingBrowser.remove();
+ }
+ document.querySelector("#print-selection-container").hidden = !this
+ .hasSelection;
+
+ let sourcePrincipal =
+ sourceBrowsingContext.currentWindowGlobal.documentPrincipal;
+ let sourceIsPdf =
+ !sourcePrincipal.isNullPrincipal && sourcePrincipal.spec == PDF_JS_URI;
+ this.originalSourceContentTitle =
+ sourceBrowsingContext.currentWindowContext.documentTitle;
+ this.originalSourceCurrentURI =
+ sourceBrowsingContext.currentWindowContext.documentURI.spec;
+
+ this.sourceWindowId = this.printFrameOnly
+ ? sourceBrowsingContext.currentWindowGlobal.outerWindowId
+ : sourceBrowsingContext.top.embedderElement.browsingContext
+ .currentWindowGlobal.outerWindowId;
+ this.selectionWindowId =
+ sourceBrowsingContext.currentWindowGlobal.outerWindowId;
+
+ // 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;
+
+ 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.getSourceBrowsingContext().top.embedderElement;
+ let dialogBoxManager = gBrowser
+ .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();
+ settings.showPrintProgress = false;
+ // We set the title so that if the user chooses save-to-PDF from the
+ // system dialog the title will be used to generate the prepopulated
+ // filename in the file picker.
+ settings.title = this.previewBrowser.browsingContext.embedderElement.contentTitle;
+ const PRINTPROMPTSVC = Cc[
+ "@mozilla.org/embedcomp/printingprompt-service;1"
+ ].getService(Ci.nsIPrintingPromptService);
+ try {
+ Services.telemetry.scalarAdd(
+ "printing.dialog_opened_via_preview_tm",
+ 1
+ );
+ await this._showPrintDialog(PRINTPROMPTSVC, window, settings);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ Services.telemetry.scalarAdd(
+ "printing.dialog_via_preview_cancelled_tm",
+ 1
+ );
+ window.close();
+ return; // user cancelled
+ }
+ throw e;
+ }
+ await this.print(settings);
+ });
+
+ let settingsToChange = await this.refreshSettings(selectedPrinter.value);
+ await this.updateSettings(settingsToChange, true);
+
+ 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,
+ })
+ );
+
+ await document.l10n.translateElements([this.previewBrowser]);
+ if (this.selectionPreviewBrowser) {
+ await document.l10n.translateElements([this.selectionPreviewBrowser]);
+ }
+
+ document.body.removeAttribute("loading");
+
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ // Now that we're showing the form, select the destination select.
+ window.focus();
+ let fm = Services.focus;
+ fm.setFocus(document.getElementById("printer-picker"), fm.FLAG_SHOWRING);
+
+ await initialPreviewDone;
+ },
+
+ unload() {
+ this.previewBrowser.frameLoader.exitPrintPreview();
+ if (this.selectionPreviewBrowser) {
+ this.selectionPreviewBrowser.frameLoader.exitPrintPreview();
+ }
+ },
+
+ 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.originalSourceContentTitle,
+ this.originalSourceCurrentURI
+ );
+ } catch (e) {
+ return false;
+ }
+ }
+
+ await window._initialized;
+
+ // This seems like it should be handled automatically but it isn't.
+ Services.prefs.setStringPref("print_printer", settings.printerName);
+
+ try {
+ // We'll provide our own progress indicator.
+ this.settings.showPrintProgress = false;
+ 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.currentPreviewBrowser.browsingContext;
+ await this._doPrint(bc, settings);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ window.close();
+ return true;
+ },
+
+ cancelPrint() {
+ Services.telemetry.scalarAdd("printing.preview_cancelled_tm", 1);
+ window.close();
+ },
+
+ async refreshSettings(printerName) {
+ 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.printInColor != this._userChangedSettings.printInColor
+ ) {
+ delete this._userChangedSettings.printInColor;
+ }
+
+ // See if the paperId needs to change.
+ let paperId = settingsToUpdate.paperId || this.viewSettings.paperId;
+
+ logger.debug("Using paperId: ", paperId);
+ logger.debug(
+ "Available paper sizes: ",
+ PrintSettingsViewProxy.availablePaperSizes
+ );
+ let matchedPaper =
+ paperId && PrintSettingsViewProxy.availablePaperSizes[paperId];
+ if (!matchedPaper) {
+ let paperWidth, paperHeight, paperSizeUnit;
+ if (settingsToUpdate.paperId) {
+ // The user changed paperId in this instance and session,
+ // We should have details on the paper size from the previous printer
+ paperId = settingsToUpdate.paperId;
+ let cachedPaperWrapper = this.allPaperSizes[paperId];
+ // for the purposes of finding a best-size match, we'll use mm
+ paperWidth = cachedPaperWrapper.paper.width * MM_PER_POINT;
+ paperHeight = cachedPaperWrapper.paper.height * MM_PER_POINT;
+ paperSizeUnit = PrintEventHandler.settings.kPaperSizeMillimeters;
+ } else {
+ paperId = this.viewSettings.paperId;
+ paperWidth = this.viewSettings.paperWidth;
+ paperHeight = this.viewSettings.paperHeight;
+ paperSizeUnit = this.viewSettings.paperSizeUnit;
+ }
+ matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
+ paperWidth,
+ paperHeight,
+ paperSizeUnit
+ );
+ }
+ if (!matchedPaper) {
+ // We didn't find a good match. Take the first paper size
+ matchedPaper = Object.values(
+ PrintSettingsViewProxy.availablePaperSizes
+ )[0];
+ delete this._userChangedSettings.paperId;
+ }
+ if (matchedPaper.id !== paperId) {
+ // The exact paper id doesn't exist for this printer
+ logger.log(
+ `Requested paperId: "${paperId}" missing on this printer, using: ${matchedPaper.id} instead`
+ );
+ delete this._userChangedSettings.paperId;
+ }
+ // Always write paper details back to settings
+ settingsToUpdate.paperId = matchedPaper.id;
+
+ return settingsToUpdate;
+ },
+
+ _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;
+
+ if (
+ parseFloat(this.viewSettings.customMargins.marginTop) +
+ parseFloat(this.viewSettings.customMargins.marginBottom) >
+ height -
+ paperWrapper.unwriteableMarginTop -
+ paperWrapper.unwriteableMarginBottom ||
+ this.viewSettings.customMargins.marginTop < 0 ||
+ this.viewSettings.customMargins.marginBottom < 0
+ ) {
+ let { marginTop, marginBottom } = this.viewSettings.defaultMargins;
+ changedSettings.marginTop = changedSettings.customMarginTop = marginTop;
+ changedSettings.marginBottom = changedSettings.customMarginBottom = marginBottom;
+ delete this._userChangedSettings.customMargins;
+ }
+
+ if (
+ parseFloat(this.viewSettings.customMargins.marginRight) +
+ parseFloat(this.viewSettings.customMargins.marginLeft) >
+ width -
+ paperWrapper.unwriteableMarginRight -
+ paperWrapper.unwriteableMarginLeft ||
+ this.viewSettings.customMargins.marginLeft < 0 ||
+ this.viewSettings.customMargins.marginRight < 0
+ ) {
+ let { marginLeft, marginRight } = this.viewSettings.defaultMargins;
+ changedSettings.marginLeft = changedSettings.customMarginLeft = marginLeft;
+ changedSettings.marginRight = changedSettings.customMarginRight = marginRight;
+ delete this._userChangedSettings.customMargins;
+ }
+ } catch (e) {
+ this.reportPrintingError("PAPER_MARGINS");
+ throw e;
+ }
+ }
+
+ for (let [setting, value] of Object.entries(changedSettings)) {
+ // Always write paper changes back to settings as pref-derived values could be bad
+ if (
+ this.viewSettings[setting] != value ||
+ (printerChanged && setting == "paperId")
+ ) {
+ 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.savePrintSettingsToPrefs(this.settings, true, flags);
+ },
+
+ /**
+ * Queue a task to update the print preview. It will start immediately or when
+ * the in progress update completes.
+ */
+ async updatePrintPreview() {
+ // Make sure the rendering state is set so we don't visibly update the
+ // sheet count with incomplete data.
+ this._showRenderingIndicator();
+ this._updatePrintPreviewTask.arm();
+ },
+
+ /**
+ * Creates a print preview or refreshes the preview with new settings when omitted.
+ *
+ * @return {Promise} Resolves when the preview has been updated.
+ */
+ async _updatePrintPreview() {
+ let { settings } = this;
+ let { printSelectionOnly } = this.viewSettings;
+ if (!this.selectionPreviewBrowser) {
+ printSelectionOnly = false;
+ }
+
+ // We never want the progress dialog to show
+ settings.showPrintProgress = false;
+
+ this._showRenderingIndicator();
+
+ let sourceWinId;
+
+ // If it's the first time loading this type of browser, get the stored window id.
+ if (printSelectionOnly && !this._hasRenderedSelectionPreview) {
+ sourceWinId = this.selectionWindowId;
+ this._hasRenderedSelectionPreview = true;
+ } else if (!printSelectionOnly && !this._hasRenderedPrimaryPreview) {
+ sourceWinId = this.sourceWindowId;
+ this._hasRenderedPrimaryPreview = true;
+ }
+
+ this.previewBrowser.parentElement.setAttribute(
+ "previewtype",
+ printSelectionOnly ? "selection" : "primary"
+ );
+
+ this.currentPreviewBrowser = printSelectionOnly
+ ? this.selectionPreviewBrowser
+ : this.previewBrowser;
+
+ 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;
+ try {
+ // This resolves with a PrintPreviewSuccessInfo dictionary.
+ ({
+ totalPageCount,
+ sheetCount,
+ isEmpty,
+ } = await this.currentPreviewBrowser.frameLoader.printPreview(
+ settings,
+ sourceWinId
+ ));
+ } catch (e) {
+ this.reportPrintingError("PRINT_PREVIEW");
+ throw e;
+ }
+
+ 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();
+ }
+
+ // Update the settings print options on whether there is a selection.
+ settings.isPrintSelectionRBEnabled = this.hasSelection;
+
+ document.dispatchEvent(
+ new CustomEvent("page-count", {
+ detail: { sheetCount, totalPages: totalPageCount },
+ })
+ );
+ this.currentPreviewBrowser.setAttribute("sheet-count", sheetCount);
+
+ this._hideRenderingIndicator();
+
+ if (isFirstCall) {
+ const elapsed = Date.now() - this.printInitiationTime;
+ Services.telemetry
+ .getHistogramById("PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS")
+ .add(elapsed);
+ }
+ },
+
+ _showRenderingIndicator() {
+ let stack = this.previewBrowser.parentElement;
+ stack.setAttribute("rendering", true);
+ document.body.setAttribute("rendering", true);
+ },
+
+ _hideRenderingIndicator() {
+ let stack = this.previewBrowser.parentElement;
+ stack.removeAttribute("rendering");
+ document.body.removeAttribute("rendering");
+ },
+
+ getSourceBrowsingContext() {
+ let params = new URLSearchParams(location.search);
+ let browsingContextId = params.get("browsingContextId");
+ if (!browsingContextId) {
+ return null;
+ }
+ return BrowsingContext.get(browsingContextId);
+ },
+
+ async getPrintDestinations() {
+ const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
+ Ci.nsIPrinterList
+ );
+ let printers;
+
+ if (Cu.isInAutomation) {
+ printers = await Promise.resolve(window._mockPrinters || []);
+ } else {
+ try {
+ printers = await printerList.printers;
+ } catch (e) {
+ this.reportPrintingError("PRINTER_LIST");
+ throw e;
+ }
+ }
+
+ let fallbackPaperList;
+ try {
+ fallbackPaperList = await printerList.fallbackPaperList;
+ } catch (e) {
+ this.reportPrintingError("FALLBACK_PAPER_LIST");
+ throw e;
+ }
+
+ let lastUsedPrinterName;
+ try {
+ lastUsedPrinterName = PSSVC.lastUsedPrinterName;
+ } catch (e) {
+ this.reportPrintingError("LAST_USED_PRINTER");
+ throw e;
+ }
+ const defaultPrinterName = printerList.systemDefaultPrinterName;
+ const printersByName = {};
+
+ let lastUsedPrinter;
+ let defaultSystemPrinter;
+
+ let saveToPdfPrinter = {
+ nameId: "printui-destination-pdf-label",
+ value: PrintUtils.SAVE_TO_PDF_PRINTER,
+ };
+ printersByName[PrintUtils.SAVE_TO_PDF_PRINTER] = {
+ supportsColor: true,
+ supportsMonochrome: false,
+ name: PrintUtils.SAVE_TO_PDF_PRINTER,
+ };
+
+ if (lastUsedPrinterName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ lastUsedPrinter = saveToPdfPrinter;
+ }
+
+ let destinations = [
+ saveToPdfPrinter,
+ ...printers.map(printer => {
+ printer.QueryInterface(Ci.nsIPrinter);
+ const { name } = printer;
+ printersByName[printer.name] = { printer };
+ const destination = { name, value: name };
+
+ if (name == lastUsedPrinterName) {
+ lastUsedPrinter = destination;
+ }
+ if (name == defaultPrinterName) {
+ defaultSystemPrinter = destination;
+ }
+
+ return destination;
+ }),
+ ];
+
+ let selectedPrinter =
+ lastUsedPrinter || defaultSystemPrinter || saveToPdfPrinter;
+
+ return {
+ destinations,
+ fallbackPaperList,
+ selectedPrinter,
+ printersByName,
+ defaultSystemPrinter,
+ };
+ },
+
+ getMarginPresets(marginSize, paper) {
+ switch (marginSize) {
+ case "minimum":
+ return {
+ marginTop: paper.unwriteableMarginTop,
+ marginLeft: paper.unwriteableMarginLeft,
+ marginBottom: paper.unwriteableMarginBottom,
+ marginRight: paper.unwriteableMarginRight,
+ };
+ case "none":
+ return {
+ marginTop: 0,
+ marginLeft: 0,
+ marginBottom: 0,
+ marginRight: 0,
+ };
+ case "custom":
+ return {
+ marginTop:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginTop ??
+ this.settings.marginTop,
+ marginBottom:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginBottom ??
+ this.settings.marginBottom,
+ marginLeft:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginLeft ??
+ this.settings.marginLeft,
+ marginRight:
+ PrintSettingsViewProxy._lastCustomMarginValues.marginRight ??
+ this.settings.marginRight,
+ };
+ default: {
+ let minimum = this.getMarginPresets("minimum", paper);
+ return {
+ marginTop: !isNaN(minimum.marginTop)
+ ? Math.max(minimum.marginTop, this.defaultSettings.marginTop)
+ : this.defaultSettings.marginTop,
+ marginRight: !isNaN(minimum.marginRight)
+ ? Math.max(minimum.marginRight, this.defaultSettings.marginRight)
+ : this.defaultSettings.marginRight,
+ marginBottom: !isNaN(minimum.marginBottom)
+ ? Math.max(minimum.marginBottom, this.defaultSettings.marginBottom)
+ : this.defaultSettings.marginBottom,
+ marginLeft: !isNaN(minimum.marginLeft)
+ ? Math.max(minimum.marginLeft, this.defaultSettings.marginLeft)
+ : this.defaultSettings.marginLeft,
+ };
+ }
+ }
+ },
+
+ reportPrintingError(aMessage) {
+ Services.telemetry.keyedScalarAdd("printing.error", aMessage, 1);
+ },
+
+ /**
+ * Prints the window. This method has been abstracted into a helper for
+ * testing purposes.
+ */
+ _doPrint(aBrowsingContext, aSettings) {
+ return aBrowsingContext.top.embedderElement.print(
+ aBrowsingContext.currentWindowGlobal.outerWindowId,
+ aSettings
+ );
+ },
+
+ /**
+ * Shows the system dialog. This method has been abstracted into a helper for
+ * testing purposes. The showPrintDialog() call blocks until the dialog is
+ * closed, so we mark it as async to allow us to reject from the test.
+ */
+ async _showPrintDialog(aPrintingPromptService, aWindow, aSettings) {
+ return aPrintingPromptService.showPrintDialog(aWindow, aSettings);
+ },
+};
+
+var PrintSettingsViewProxy = {
+ get defaultHeadersAndFooterValues() {
+ const defaultBranch = Services.prefs.getDefaultBranch("");
+ let settingValues = {};
+ for (let [name, pref] of Object.entries(this.headerFooterSettingsPrefs)) {
+ settingValues[name] = defaultBranch.getStringPref(pref);
+ }
+ // We only need to retrieve these defaults once and they will not change
+ Object.defineProperty(this, "defaultHeadersAndFooterValues", {
+ value: settingValues,
+ });
+ return settingValues;
+ },
+
+ headerFooterSettingsPrefs: {
+ footerStrCenter: "print.print_footercenter",
+ footerStrLeft: "print.print_footerleft",
+ footerStrRight: "print.print_footerright",
+ headerStrCenter: "print.print_headercenter",
+ headerStrLeft: "print.print_headerleft",
+ headerStrRight: "print.print_headerright",
+ },
+
+ // Custom margins are not saved by a pref, so we need to keep track of them
+ // in order to save the value.
+ _lastCustomMarginValues: {
+ marginTop: null,
+ marginBottom: null,
+ marginLeft: null,
+ marginRight: null,
+ },
+
+ // This list was taken from nsDeviceContextSpecWin.cpp which records telemetry on print target type
+ knownSaveToFilePrinters: new Set([
+ "Microsoft Print to PDF",
+ "Adobe PDF",
+ "Bullzip PDF Printer",
+ "CutePDF Writer",
+ "doPDF",
+ "Foxit Reader PDF Printer",
+ "Nitro PDF Creator",
+ "novaPDF",
+ "PDF-XChange",
+ "PDF24 PDF",
+ "PDFCreator",
+ "PrimoPDF",
+ "Soda PDF",
+ "Solid PDF Creator",
+ "Universal Document Converter",
+ "Microsoft XPS Document Writer",
+ ]),
+
+ getBestPaperMatch(paperWidth, paperHeight, paperSizeUnit) {
+ let paperSizes = Object.values(this.availablePaperSizes);
+ if (!(paperWidth && paperHeight)) {
+ return null;
+ }
+ // first try to match on the paper dimensions using the current units
+ let unitsPerPoint;
+ let altUnitsPerPoint;
+ if (paperSizeUnit == PrintEventHandler.settings.kPaperSizeMillimeters) {
+ unitsPerPoint = MM_PER_POINT;
+ altUnitsPerPoint = INCHES_PER_POINT;
+ } else {
+ unitsPerPoint = INCHES_PER_POINT;
+ altUnitsPerPoint = MM_PER_POINT;
+ }
+ // equality to 1pt.
+ const equal = (a, b) => Math.abs(a - b) < 1;
+ const findMatch = (widthPts, heightPts) =>
+ paperSizes.find(paperWrapper => {
+ // the dimensions on the nsIPaper object are in points
+ let result =
+ equal(widthPts, paperWrapper.paper.width) &&
+ equal(heightPts, paperWrapper.paper.height);
+ return result;
+ });
+ // Look for a paper with matching dimensions, using the current printer's
+ // paper size unit, then the alternate unit
+ let matchedPaper =
+ findMatch(paperWidth / unitsPerPoint, paperHeight / unitsPerPoint) ||
+ findMatch(paperWidth / altUnitsPerPoint, paperHeight / altUnitsPerPoint);
+
+ if (matchedPaper) {
+ return matchedPaper;
+ }
+ return null;
+ },
+
+ async fetchPaperMargins(paperId) {
+ // resolve any async and computed properties we need on the paper
+ let paperWrapper = this.availablePaperSizes[paperId];
+ if (!paperWrapper) {
+ throw new Error("Can't fetchPaperMargins: " + paperId);
+ }
+ if (paperWrapper._resolved) {
+ // We've already resolved and calculated these values
+ return paperWrapper;
+ }
+ let margins;
+ try {
+ margins = await paperWrapper.paper.unwriteableMargin;
+ } catch (e) {
+ this.reportPrintingError("UNWRITEABLE_MARGIN");
+ throw e;
+ }
+ margins.QueryInterface(Ci.nsIPaperMargin);
+
+ // margin dimensions are given on the paper in points, setting values need to be in inches
+ paperWrapper.unwriteableMarginTop = margins.top * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginRight = margins.right * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginBottom = margins.bottom * INCHES_PER_POINT;
+ paperWrapper.unwriteableMarginLeft = margins.left * INCHES_PER_POINT;
+ // No need to re-resolve static properties
+ paperWrapper._resolved = true;
+ return paperWrapper;
+ },
+
+ async resolvePropertiesForPrinter(printerName) {
+ // resolve any async properties we need on the printer
+ let printerInfo = this.availablePrinters[printerName];
+ if (printerInfo._resolved) {
+ // Store a convenience reference
+ this.availablePaperSizes = printerInfo.availablePaperSizes;
+ return printerInfo;
+ }
+
+ // Await the async printer data.
+ if (printerInfo.printer) {
+ let basePrinterInfo;
+ try {
+ [
+ printerInfo.supportsDuplex,
+ printerInfo.supportsColor,
+ printerInfo.supportsMonochrome,
+ basePrinterInfo,
+ ] = await Promise.all([
+ printerInfo.printer.supportsDuplex,
+ printerInfo.printer.supportsColor,
+ printerInfo.printer.supportsMonochrome,
+ printerInfo.printer.printerInfo,
+ ]);
+ } catch (e) {
+ this.reportPrintingError("PRINTER_SETTINGS");
+ throw e;
+ }
+ basePrinterInfo.QueryInterface(Ci.nsIPrinterInfo);
+ basePrinterInfo.defaultSettings.QueryInterface(Ci.nsIPrintSettings);
+
+ printerInfo.paperList = basePrinterInfo.paperList;
+ printerInfo.defaultSettings = basePrinterInfo.defaultSettings;
+ } else if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ // The Mozilla PDF pseudo-printer has no actual nsIPrinter implementation
+ printerInfo.defaultSettings = PSSVC.newPrintSettings;
+ printerInfo.defaultSettings.printerName = printerName;
+ printerInfo.defaultSettings.toFileName = "";
+ printerInfo.defaultSettings.outputFormat =
+ Ci.nsIPrintSettings.kOutputFormatPDF;
+ printerInfo.defaultSettings.printToFile = true;
+ printerInfo.paperList = this.fallbackPaperList;
+ }
+ printerInfo.settings = printerInfo.defaultSettings.clone();
+ // Apply any previously persisted user values
+ let flags = printerInfo.settings.kInitSaveAll;
+ if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
+ // Don't apply potentially-bad printToFile setting that may be in some user's prefs.
+ flags ^= printerInfo.settings.kInitSavePrintToFile;
+ }
+ PSSVC.initPrintSettingsFromPrefs(printerInfo.settings, true, flags);
+ // We set `isInitializedFromPrinter` to make sure that that's set on the
+ // SAVE_TO_PDF_PRINTER settings. The naming is poor, but that tells the
+ // platform code that the settings object is complete.
+ printerInfo.settings.isInitializedFromPrinter = true;
+
+ printerInfo.settings.toFileName = "";
+
+ // prepare the available paper sizes for this printer
+ if (!printerInfo.paperList?.length) {
+ logger.warn(
+ "Printer has empty paperList: ",
+ printerInfo.printer.id,
+ "using fallbackPaperList"
+ );
+ printerInfo.paperList = this.fallbackPaperList;
+ }
+ // don't trust the settings to provide valid paperSizeUnit values
+ let sizeUnit =
+ printerInfo.settings.paperSizeUnit ==
+ printerInfo.settings.kPaperSizeMillimeters
+ ? printerInfo.settings.kPaperSizeMillimeters
+ : printerInfo.settings.kPaperSizeInches;
+ let papersById = (printerInfo.availablePaperSizes = {});
+ // Store a convenience reference
+ this.availablePaperSizes = papersById;
+
+ for (let paper of printerInfo.paperList) {
+ paper.QueryInterface(Ci.nsIPaper);
+ // Bug 1662239: I'm seeing multiple duplicate entries for each paper size
+ // so ensure we have one entry per name
+ if (!papersById[paper.id]) {
+ papersById[paper.id] = {
+ paper,
+ id: paper.id,
+ name: paper.name,
+ // XXXsfoster: Eventually we want to get the unit from the nsIPaper object
+ sizeUnit,
+ };
+ }
+ }
+ // Update our cache of all the paper sizes by name
+ Object.assign(PrintEventHandler.allPaperSizes, papersById);
+
+ // The printer properties don't change, mark this as resolved for next time
+ printerInfo._resolved = true;
+ return printerInfo;
+ },
+
+ get(target, name) {
+ switch (name) {
+ case "currentPaper": {
+ let paperId = this.get(target, "paperId");
+ return paperId && this.availablePaperSizes[paperId];
+ }
+
+ case "marginPresets":
+ let paperWrapper = this.get(target, "currentPaper");
+ return {
+ none: PrintEventHandler.getMarginPresets("none", paperWrapper),
+ minimum: PrintEventHandler.getMarginPresets("minimum", paperWrapper),
+ default: PrintEventHandler.getMarginPresets("default", paperWrapper),
+ custom: PrintEventHandler.getMarginPresets("custom", paperWrapper),
+ };
+
+ case "marginOptions": {
+ let allMarginPresets = this.get(target, "marginPresets");
+ let uniqueMargins = new Set();
+ let marginsEnabled = {};
+ for (let name of ["none", "default", "minimum", "custom"]) {
+ let {
+ marginTop,
+ marginLeft,
+ marginBottom,
+ marginRight,
+ } = allMarginPresets[name];
+ let key = [marginTop, marginLeft, marginBottom, marginRight].join(
+ ","
+ );
+ // Custom margins are initialized to default margins
+ marginsEnabled[name] = !uniqueMargins.has(key) || name == "custom";
+ uniqueMargins.add(key);
+ }
+ return marginsEnabled;
+ }
+
+ case "margins":
+ let marginSettings = {
+ marginTop: target.marginTop,
+ marginLeft: target.marginLeft,
+ marginBottom: target.marginBottom,
+ marginRight: target.marginRight,
+ };
+ // see if they match the none, minimum, or default margin values
+ let allMarginPresets = this.get(target, "marginPresets");
+ for (let presetName of ["none", "minimum", "default"]) {
+ let marginPresets = allMarginPresets[presetName];
+ if (
+ Object.keys(marginSettings).every(
+ name =>
+ marginSettings[name].toFixed(2) ==
+ marginPresets[name].toFixed(2)
+ )
+ ) {
+ return presetName;
+ }
+ }
+
+ // Fall back to custom for other values
+ return "custom";
+
+ case "defaultMargins":
+ return PrintEventHandler.getMarginPresets(
+ "default",
+ this.get(target, "currentPaper")
+ );
+
+ case "customMargins":
+ return PrintEventHandler.getMarginPresets(
+ "custom",
+ this.get(target, "currentPaper")
+ );
+
+ case "paperSizes":
+ return Object.values(this.availablePaperSizes)
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map(paper => {
+ return {
+ name: paper.name,
+ value: paper.id,
+ };
+ });
+
+ case "supportsDuplex":
+ return this.availablePrinters[target.printerName].supportsDuplex;
+
+ case "printDuplex":
+ return target.duplex;
+
+ case "printBackgrounds":
+ return target.printBGImages || target.printBGColors;
+
+ case "printFootersHeaders":
+ // if any of the footer and headers settings have a non-empty string value
+ // we consider that "enabled"
+ return Object.keys(this.headerFooterSettingsPrefs).some(
+ name => !!target[name]
+ );
+
+ case "supportsColor":
+ return this.availablePrinters[target.printerName].supportsColor;
+
+ case "willSaveToFile":
+ return (
+ target.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF ||
+ this.knownSaveToFilePrinters.has(target.printerName)
+ );
+ case "supportsMonochrome":
+ return this.availablePrinters[target.printerName].supportsMonochrome;
+ case "defaultSystemPrinter":
+ return (
+ this.defaultSystemPrinter?.value ||
+ Object.getOwnPropertyNames(this.availablePrinters).find(
+ name => name != PrintUtils.SAVE_TO_PDF_PRINTER
+ )
+ );
+
+ case "numCopies":
+ return this.get(target, "willSaveToFile") ? 1 : target.numCopies;
+ }
+ return target[name];
+ },
+
+ set(target, name, value) {
+ switch (name) {
+ case "margins":
+ if (!["default", "minimum", "none", "custom"].includes(value)) {
+ logger.warn("Unexpected margin preset name: ", value);
+ value = "default";
+ }
+ let paperWrapper = this.get(target, "currentPaper");
+ let marginPresets = PrintEventHandler.getMarginPresets(
+ value,
+ paperWrapper
+ );
+ for (let [settingName, presetValue] of Object.entries(marginPresets)) {
+ target[settingName] = presetValue;
+ }
+ break;
+
+ case "paperId": {
+ let paperId = value;
+ let paperWrapper = this.availablePaperSizes[paperId];
+ // Dimensions on the paper object are in pts.
+ // We convert to the printer's specified unit when updating settings
+ let unitsPerPoint =
+ paperWrapper.sizeUnit == target.kPaperSizeMillimeters
+ ? MM_PER_POINT
+ : INCHES_PER_POINT;
+ // paperWidth and paperHeight are calculated values that we always treat as suspect and
+ // re-calculate whenever the paperId changes
+ target.paperSizeUnit = paperWrapper.sizeUnit;
+ target.paperWidth = paperWrapper.paper.width * unitsPerPoint;
+ target.paperHeight = paperWrapper.paper.height * unitsPerPoint;
+ // Unwriteable margins were pre-calculated from their async values when the paper size
+ // was selected. They are always in inches
+ target.unwriteableMarginTop = paperWrapper.unwriteableMarginTop;
+ target.unwriteableMarginRight = paperWrapper.unwriteableMarginRight;
+ target.unwriteableMarginBottom = paperWrapper.unwriteableMarginBottom;
+ target.unwriteableMarginLeft = paperWrapper.unwriteableMarginLeft;
+ target.paperId = paperWrapper.paper.id;
+ // pull new margin values for the new paper size
+ this.set(target, "margins", this.get(target, "margins"));
+ break;
+ }
+
+ case "printerName":
+ // Can't set printerName, settings objects belong to a specific printer.
+ break;
+
+ case "printBackgrounds":
+ target.printBGImages = value;
+ target.printBGColors = value;
+ break;
+
+ case "printDuplex":
+ target.duplex = value
+ ? Ci.nsIPrintSettings.kDuplexHorizontal
+ : Ci.nsIPrintSettings.kSimplex;
+ break;
+
+ case "printFootersHeaders":
+ // To disable header & footers, set them all to empty.
+ // To enable, restore default values for each of the header & footer settings.
+ for (let [settingName, defaultValue] of Object.entries(
+ this.defaultHeadersAndFooterValues
+ )) {
+ target[settingName] = value ? defaultValue : "";
+ }
+ break;
+
+ case "customMargins":
+ if (value != null) {
+ for (let [settingName, newVal] of Object.entries(value)) {
+ target[settingName] = newVal;
+ this._lastCustomMarginValues[settingName] = newVal;
+ }
+ }
+ break;
+
+ case "customMarginTop":
+ case "customMarginBottom":
+ case "customMarginLeft":
+ case "customMarginRight":
+ let customMarginName = "margin" + name.substring(12);
+ this.set(
+ target,
+ "customMargins",
+ Object.assign({}, this.get(target, "customMargins"), {
+ [customMarginName]: value,
+ })
+ );
+ break;
+
+ default:
+ target[name] = value;
+ }
+ },
+};
+
+/*
+ * Custom elements ----------------------------------------------------
+ */
+
+function PrintUIControlMixin(superClass) {
+ return class PrintUIControl extends superClass {
+ connectedCallback() {
+ this.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");
+ if (AppConstants.platform != "win") {
+ // Move the Print button to the end if this isn't Windows.
+ this.printButton.parentElement.append(this.printButton);
+ }
+ this.querySelector("#pages-per-sheet").hidden = !Services.prefs.getBoolPref(
+ "print.pages_per_sheet.enabled",
+ false
+ );
+ }
+
+ removeNonPdfSettings() {
+ let selectors = [
+ "#margins",
+ "#headers-footers",
+ "#backgrounds",
+ "#print-selection-container",
+ ];
+ 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("#copies").hidden = settings.willSaveToFile;
+
+ 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("keypress", e => this.handleKeypress(e));
+ this.addEventListener("paste", e => this.handlePaste(e));
+ }
+
+ connectedCallback() {
+ this.type = "number";
+ this.settingName = this.dataset.settingName;
+ super.connectedCallback();
+ }
+
+ update(settings) {
+ if (this.settingName) {
+ this.value = settings[this.settingName];
+ }
+ }
+
+ handleKeypress(e) {
+ let char = String.fromCharCode(e.charCode);
+ let acceptedChar = e.target.step.includes(".")
+ ? char.match(/^[0-9.]$/)
+ : char.match(/^[0-9]$/);
+ if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ }
+ }
+
+ handlePaste(e) {
+ let paste = (e.clipboardData || window.clipboardData)
+ .getData("text")
+ .trim();
+ let acceptedChars = e.target.step.includes(".")
+ ? paste.match(/^[0-9.]*$/)
+ : paste.match(/^[0-9]*$/);
+ if (!acceptedChars) {
+ e.preventDefault();
+ }
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "paste":
+ this.handlePaste();
+ break;
+ case "keypress":
+ this.handleKeypress();
+ break;
+ 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 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;
+ }
+
+ update(settings) {
+ if (settings.printerName !== this._printerName) {
+ this._printerName = settings.printerName;
+ this.setOptions(settings.paperSizes);
+ }
+ this.value = settings.paperId;
+ }
+}
+customElements.define("paper-size-select", PaperSizePicker, {
+ extends: "select",
+});
+
+class OrientationInput extends PrintUIControlMixin(HTMLElement) {
+ get templateId() {
+ return "orientation-template";
+ }
+
+ update(settings) {
+ for (let input of this.querySelectorAll("input")) {
+ input.checked = settings.orientation == input.value;
+ }
+ }
+
+ handleEvent(e) {
+ this.dispatchSettingsChange({
+ orientation: e.target.value,
+ });
+ }
+}
+customElements.define("orientation-input", OrientationInput);
+
+class 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._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 isAll = this._rangePicker.value == "all";
+ if (isAll) {
+ this._pagesSet.clear();
+ if (!this._rangeInput.checkValidity()) {
+ this._rangeInput.setCustomValidity("");
+ this._rangeInput.value = "";
+ }
+ } else {
+ this.validateRangeInput();
+ }
+
+ 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 || isAll) {
+ 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 == "all") {
+ 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._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 = this._rangePicker.value == "all" ? "" : this._rangeInput.value;
+ this._validateRangeInput(value, this._numPages);
+ }
+
+ 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;
+
+ 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 == "all";
+ this.updatePageRange();
+ this.dispatchPageRange();
+ } 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");
+ }
+
+ get templateId() {
+ return "margins-template";
+ }
+
+ updateCustomMargins() {
+ let newMargins = {
+ marginTop: this._customTopMargin.value,
+ marginBottom: this._customBottomMargin.value,
+ marginLeft: this._customLeftMargin.value,
+ marginRight: this._customRightMargin.value,
+ };
+
+ this.dispatchSettingsChange({
+ margins: "custom",
+ customMargins: newMargins,
+ });
+ this._marginError.hidden = true;
+ }
+
+ updateMaxValues() {
+ this._customTopMargin.max =
+ this._maxHeight - this._customBottomMargin.value;
+ this._customBottomMargin.max =
+ this._maxHeight - this._customTopMargin.value;
+ this._customLeftMargin.max = this._maxWidth - this._customRightMargin.value;
+ this._customRightMargin.max = this._maxWidth - this._customLeftMargin.value;
+ }
+
+ formatMargin(target) {
+ if (target.value.includes(".")) {
+ if (target.value.split(".")[1].length > 2) {
+ let dotIndex = target.value.indexOf(".");
+ target.value = target.value.slice(0, dotIndex + 3);
+ }
+ }
+ }
+
+ setAllMarginValues(settings) {
+ this._customTopMargin.value = parseFloat(
+ settings.customMargins.marginTop
+ ).toFixed(2);
+ this._customBottomMargin.value = parseFloat(
+ settings.customMargins.marginBottom
+ ).toFixed(2);
+ this._customLeftMargin.value = parseFloat(
+ settings.customMargins.marginLeft
+ ).toFixed(2);
+ this._customRightMargin.value = parseFloat(
+ settings.customMargins.marginRight
+ ).toFixed(2);
+ }
+
+ update(settings) {
+ // Re-evaluate which margin options should be enabled whenever the printer or paper changes
+ if (
+ settings.paperId !== this._paperId ||
+ settings.printerName !== this._printerName ||
+ settings.orientation !== this._orientation
+ ) {
+ let enabledMargins = settings.marginOptions;
+ for (let option of this._marginPicker.options) {
+ option.hidden = !enabledMargins[option.value];
+ }
+ this._paperId = settings.paperId;
+ this._printerName = settings.printerName;
+ this._orientation = settings.orientation;
+
+ let height =
+ this._orientation == 0 ? settings.paperHeight : settings.paperWidth;
+ let width =
+ this._orientation == 0 ? settings.paperWidth : settings.paperHeight;
+
+ this._maxHeight =
+ height -
+ settings.unwriteableMarginTop -
+ settings.unwriteableMarginBottom;
+ this._maxWidth =
+ width -
+ settings.unwriteableMarginLeft -
+ settings.unwriteableMarginRight;
+
+ this._defaultPresets = settings.defaultMargins;
+ // The values in custom fields should be initialized to custom margin values
+ // and must be overriden if they are no longer valid.
+ this.setAllMarginValues(settings);
+ this.updateMaxValues();
+ this.dispatchEvent(new Event("revalidate", { bubbles: true }));
+ this._marginError.hidden = true;
+ }
+
+ // We need to ensure we don't override the value if the value should be custom.
+ if (this._marginPicker.value != "custom") {
+ // Reset the custom margin values if they are not valid and revalidate the form
+ if (
+ !this._customTopMargin.checkValidity() ||
+ !this._customBottomMargin.checkValidity() ||
+ !this._customLeftMargin.checkValidity() ||
+ !this._customRightMargin.checkValidity()
+ ) {
+ window.clearTimeout(this.showErrorTimeoutId);
+ this.setAllMarginValues(settings);
+ this.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);
+ this.updateSummary();
+ }
+
+ handleEvent(e) {
+ let willOpen = !this.isOpen;
+ this.updateSummary(willOpen);
+ }
+
+ updateSummary(open = false) {
+ document.l10n.setAttributes(
+ this.label,
+ open
+ ? this.getAttribute("data-open-l10n-id")
+ : this.getAttribute("data-closed-l10n-id")
+ );
+ }
+}
+customElements.define("twisty-summary", TwistySummary);
+
+class PageCount extends PrintUIControlMixin(HTMLElement) {
+ initialize() {
+ super.initialize();
+ document.addEventListener("page-count", this);
+ }
+
+ update(settings) {
+ this.numCopies = settings.numCopies;
+ this.render();
+ }
+
+ render() {
+ if (!this.numCopies || !this.sheetCount) {
+ return;
+ }
+ document.l10n.setAttributes(this, "printui-sheets-count", {
+ sheetCount: this.sheetCount * this.numCopies,
+ });
+
+ // The loading attribute must be removed on first render
+ if (this.hasAttribute("loading")) {
+ this.removeAttribute("loading");
+ }
+
+ if (this.id) {
+ // We're showing the sheet count, so let it describe the dialog.
+ document.body.setAttribute("aria-describedby", this.id);
+ }
+ }
+
+ handleEvent(e) {
+ this.sheetCount = e.detail.sheetCount;
+ this.render();
+ }
+}
+customElements.define("page-count", PageCount);
+
+class PrintButton extends PrintUIControlMixin(HTMLButtonElement) {
+ update(settings) {
+ let l10nId =
+ settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
+ ? "printui-primary-button-save"
+ : "printui-primary-button";
+ document.l10n.setAttributes(this, l10nId);
+ }
+}
+customElements.define("print-button", PrintButton, { extends: "button" });
+
+class CancelButton extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.addEventListener("click", () => {
+ this.dispatchEvent(new Event("cancel-print", { bubbles: true }));
+ });
+ }
+}
+customElements.define("cancel-button", CancelButton, { extends: "button" });
+
+async function pickFileName(contentTitle, currentURI) {
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let [title] = await document.l10n.formatMessages([
+ { id: "printui-save-to-pdf-title" },
+ ]);
+ title = title.value;
+
+ let filename;
+ if (contentTitle != "") {
+ filename = contentTitle;
+ } else {
+ let url = new URL(currentURI);
+ let path = decodeURIComponent(url.pathname);
+ path = path.replace(/\/$/, "");
+ filename = path.split("/").pop();
+ if (filename == "") {
+ filename = url.hostname;
+ }
+ }
+ if (!filename.endsWith(".pdf")) {
+ // macOS and linux don't set the extension based on the default extension.
+ // Windows won't add the extension a second time, fortunately.
+ // If it already ends with .pdf though, adding it again isn't needed.
+ filename += ".pdf";
+ }
+ filename = DownloadPaths.sanitize(filename);
+
+ picker.init(
+ window.docShell.chromeEventHandler.ownerGlobal,
+ title,
+ Ci.nsIFilePicker.modeSave
+ );
+ picker.appendFilter("PDF", "*.pdf");
+ picker.defaultExtension = "pdf";
+ picker.defaultString = filename;
+
+ let retval = await new Promise(resolve => picker.open(resolve));
+
+ if (retval == 1) {
+ throw new Error({ reason: "cancelled" });
+ } else {
+ // OK clicked (retval == 0) or replace confirmed (retval == 2)
+
+ // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
+ // the print progress listener is never called. This workaround ensures that a correct status is always returned.
+ try {
+ let fstream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
+ fstream.close();
+
+ // 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..ed02a45af1
--- /dev/null
+++ b/toolkit/components/printing/content/printPageSetup.js
@@ -0,0 +1,540 @@
+// -*- 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/. */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+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.savePrintSettingsToPrefs(gPrintSettings, true, 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..f1c7942a3b
--- /dev/null
+++ b/toolkit/components/printing/content/printPageSetup.xhtml
@@ -0,0 +1,212 @@
+<?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/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"
+ 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..fd2e7883bc
--- /dev/null
+++ b/toolkit/components/printing/content/printPagination.css
@@ -0,0 +1,143 @@
+/* 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);
+ --border-active-color: ButtonShadow;
+}
+
+: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;
+}
+
+.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(--lwt-toolbarbutton-icon-fill, currentColor);
+ fill-opacity: var(--toolbarbutton-icon-fill-opacity);
+}
+
+.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);
+}
+.toolbarButton.startItem:-moz-focusring,
+.toolbarButton.endItem:-moz-locale-dir(rtl):-moz-focusring {
+ -moz-outline-radius: 8px;
+ -moz-outline-radius-topright: 0;
+ -moz-outline-radius-bottomright: 0;
+}
+.toolbarButton.endItem:-moz-focusring,
+.toolbarButton.startItem:-moz-locale-dir(rtl):-moz-focusring {
+ -moz-outline-radius: 8px;
+ -moz-outline-radius-topleft: 0;
+ -moz-outline-radius-bottomleft: 0;
+}
+
+.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,
+#navigateEnd:-moz-locale-dir(rtl)::after,
+#navigateHome:-moz-locale-dir(ltr)::after,
+#navigateNext:-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..cd426f2f77
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewPagination.js
@@ -0,0 +1,208 @@
+// 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() {
+ if (!this._previewBrowser) {
+ // Assuming we're a sibling of our preview browser.
+ this._previewBrowser = this.parentNode.querySelector(
+ ".printPreviewBrowser"
+ );
+ }
+ return this._previewBrowser;
+ }
+
+ set previewBrowser(aBrowser) {
+ this._previewBrowser = aBrowser;
+ }
+
+ 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);
+
+ let knownAttrs = {
+ "sheet-count": "sheetCount",
+ "current-page": "currentPage",
+ };
+ this.mutationObserver = new MutationObserver(changes => {
+ let opts = {};
+ for (let change of changes) {
+ let { attributeName, target, type } = change;
+ if (type == "attributes" && attributeName in knownAttrs) {
+ opts[knownAttrs[attributeName]] = parseInt(
+ target.getAttribute(attributeName),
+ 10
+ );
+ }
+ }
+ if (opts.sheetCount || opts.currentPage) {
+ this.update(opts);
+ }
+ });
+ this.mutationObserver.observe(this.previewBrowser, {
+ attributes: ["current-page", "sheet-count"],
+ });
+
+ this.currentPreviewBrowserObserver = new MutationObserver(changes => {
+ for (let change of changes) {
+ if (change.attributeName == "previewtype") {
+ let previewType = change.target.getAttribute("previewtype");
+ this.previewBrowser = change.target.querySelector(
+ `browser[previewtype="${previewType}"]`
+ );
+ this.mutationObserver.disconnect();
+ this.mutationObserver.observe(this.previewBrowser, {
+ attributes: ["current-page", "sheet-count"],
+ });
+ }
+ }
+ });
+ this.currentPreviewBrowserObserver.observe(this.parentNode, {
+ attributes: ["previewtype"],
+ });
+
+ // 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) {
+ if (this.sheetCount !== data.sheetCount || this.currentSheet !== 1) {
+ // when sheet count changes, scroll position will get reset
+ this.currentSheet = 1;
+ }
+ 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,
+ }
+ );
+ }
+ }
+);
diff --git a/toolkit/components/printing/content/printPreviewProgress.js b/toolkit/components/printing/content/printPreviewProgress.js
new file mode 100644
index 0000000000..783f434a34
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewProgress.js
@@ -0,0 +1,152 @@
+// -*- 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/. */
+
+// dialog is just an array we'll use to store various properties from the dialog document...
+var dialog;
+
+// the printProgress is a nsIPrintProgress object
+var printProgress = null;
+
+// random global variables...
+var targetFile;
+
+var docTitle = "";
+var docURL = "";
+var progressParams = null;
+
+function ellipseString(aStr, doFront) {
+ if (!aStr) {
+ return "";
+ }
+
+ if (
+ aStr.length > 3 &&
+ (aStr.substr(0, 3) == "..." || aStr.substr(aStr.length - 4, 3) == "...")
+ ) {
+ return aStr;
+ }
+
+ var fixedLen = 64;
+ if (aStr.length <= fixedLen) {
+ return aStr;
+ }
+
+ if (doFront) {
+ return "..." + aStr.substr(aStr.length - fixedLen, fixedLen);
+ }
+
+ return aStr.substr(0, fixedLen) + "...";
+}
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ window.close();
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ if (!progressParams) {
+ return;
+ }
+ var docTitleStr = ellipseString(progressParams.docTitle, false);
+ if (docTitleStr != docTitle) {
+ docTitle = docTitleStr;
+ dialog.title.value = docTitle;
+ }
+ var docURLStr = ellipseString(progressParams.docURL, true);
+ if (docURLStr != docURL && dialog.title != null) {
+ docURL = docURLStr;
+ if (docTitle == "") {
+ dialog.title.value = docURLStr;
+ }
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {},
+ onSecurityChange(aWebProgress, aRequest, state) {},
+ onContentBlockingEvent(aWebProgress, aRequest, event) {},
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (aMessage) {
+ dialog.title.setAttribute("value", aMessage);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function onLoad() {
+ // Set global variables.
+ printProgress = window.arguments[0];
+ if (window.arguments[1]) {
+ progressParams = window.arguments[1].QueryInterface(
+ Ci.nsIPrintProgressParams
+ );
+ if (progressParams) {
+ docTitle = ellipseString(progressParams.docTitle, false);
+ docURL = ellipseString(progressParams.docURL, true);
+ }
+ }
+
+ if (!printProgress) {
+ dump("Invalid argument to printPreviewProgress.xhtml\n");
+ window.close();
+ return;
+ }
+
+ dialog = {};
+ dialog.strings = [];
+ dialog.title = document.getElementById("dialog.title");
+ dialog.titleLabel = document.getElementById("dialog.titleLabel");
+
+ dialog.title.value = docTitle;
+
+ // set our web progress listener on the helper app launcher
+ printProgress.registerListener(progressListener);
+
+ // We need to delay the set title else dom will overwrite it
+ window.setTimeout(doneIniting, 100);
+}
+
+function onUnload() {
+ if (!printProgress) {
+ return;
+ }
+ try {
+ printProgress.unregisterListener(progressListener);
+ printProgress = null;
+ } catch (e) {}
+}
+
+// If the user presses cancel, tell the app launcher and close the dialog...
+function onCancel() {
+ // Cancel app launcher.
+ try {
+ printProgress.processCanceledByUser = true;
+ } catch (e) {
+ return true;
+ }
+
+ // don't Close up dialog by returning false, the backend will close the dialog when everything will be aborted.
+ return false;
+}
+
+function doneIniting() {
+ // called by function timeout in onLoad
+ printProgress.doneIniting();
+}
diff --git a/toolkit/components/printing/content/printPreviewProgress.xhtml b/toolkit/components/printing/content/printPreviewProgress.xhtml
new file mode 100644
index 0000000000..02ab68b073
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewProgress.xhtml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+
+<!-- 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"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="print-preview-window"
+ style="width: 36em;"
+ oncancel="onCancel()"
+ onload="onLoad()"
+ onunload="onUnload()">
+<dialog buttons="cancel">
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/printing/printDialogs.ftl"/>
+ </linkset>
+
+ <script src="chrome://global/content/printPreviewProgress.js"/>
+
+ <box flex="1" style="display: grid; grid-template-columns: min-content max-content; grid-template-rows: max-content max-content;">
+ <!-- First row -->
+ <label id="dialog.titleLabel" data-l10n-id="print-title" style="justify-self: end;"/>
+ <label id="dialog.title"/>
+
+ <!-- Second row -->
+ <label id="dialog.progressLabel" data-l10n-id="print-progress" style="justify-self: end;"/>
+ <label id="dialog.progressSpaces" data-l10n-id="print-preparing"/>
+ </box>
+</dialog>
+</window>
diff --git a/toolkit/components/printing/content/printPreviewToolbar.js b/toolkit/components/printing/content/printPreviewToolbar.js
new file mode 100644
index 0000000000..b2ff4e8259
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewToolbar.js
@@ -0,0 +1,444 @@
+// 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-toolbar",
+ class PrintPreviewToolbar extends MozXULElement {
+ static get markup() {
+ return `
+ <button id="print-preview-print" oncommand="this.parentNode.print();" data-l10n-id="printpreview-print"/>
+ <button id="print-preview-pageSetup" oncommand="this.parentNode.doPageSetup();" data-l10n-id="printpreview-page-setup"/>
+ <vbox align="center" pack="center">
+ <label control="print-preview-pageNumber" data-l10n-id="printpreview-page"/>
+ </vbox>
+ <toolbarbutton id="print-preview-navigateHome" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(0, 0, 'home');" data-l10n-id="printpreview-homearrow"/>
+ <toolbarbutton id="print-preview-navigatePrevious" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(-1, 0, 0);" data-l10n-id="printpreview-previousarrow"/>
+ <hbox align="center" pack="center">
+ <html:input id="print-preview-pageNumber" hidespinbuttons="true" type="number" value="1" min="1"/>
+ <label data-l10n-id="printpreview-of"/>
+ <label id="print-preview-totalPages" value="1"/>
+ </hbox>
+ <toolbarbutton id="print-preview-navigateNext" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(1, 0, 0);" data-l10n-id="printpreview-nextarrow"/>
+ <toolbarbutton id="print-preview-navigateEnd" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(0, 0, 'end');" data-l10n-id="printpreview-endarrow"/>
+ <toolbarseparator class="toolbarseparator-primary"/>
+ <vbox align="center" pack="center">
+ <label id="print-preview-scale-label" control="print-preview-scale" data-l10n-id="printpreview-scale"/>
+ </vbox>
+ <hbox align="center" pack="center">
+ <menulist id="print-preview-scale" crop="none" oncommand="parentNode.parentNode.scale(this.selectedItem.value);">
+ <menupopup>
+ <menuitem value="0.3" />
+ <menuitem value="0.4" />
+ <menuitem value="0.5" />
+ <menuitem value="0.6" />
+ <menuitem value="0.7" />
+ <menuitem value="0.8" />
+ <menuitem value="0.9" />
+ <menuitem value="1" />
+ <menuitem value="1.25" />
+ <menuitem value="1.5" />
+ <menuitem value="1.75" />
+ <menuitem value="2" />
+ <menuseparator/>
+ <menuitem flex="1" value="ShrinkToFit" data-l10n-id="printpreview-shrink-to-fit"/>
+ <menuitem value="Custom" data-l10n-id="printpreview-custom"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <toolbarseparator class="toolbarseparator-primary"/>
+ <hbox align="center" pack="center">
+ <toolbarbutton id="print-preview-portrait-button" checked="true" type="radio" group="orient" class="tabbable" oncommand="parentNode.parentNode.orient('portrait');" data-l10n-id="printpreview-portrait"/>
+ <toolbarbutton id="print-preview-landscape-button" type="radio" group="orient" class="tabbable" oncommand="parentNode.parentNode.orient('landscape');" data-l10n-id="printpreview-landscape"/>
+ </hbox>
+ <toolbarseparator class="toolbarseparator-primary"/>
+ <checkbox id="print-preview-simplify" checked="false" disabled="true" oncommand="this.parentNode.simplify();" data-l10n-id="printpreview-simplify-page-checkbox"/>
+ <toolbarseparator class="toolbarseparator-primary"/>
+ <button id="print-preview-toolbar-close-button" oncommand="PrintUtils.exitPrintPreview();" data-l10n-id="printpreview-close"/>
+ <data id="print-preview-custom-scale-prompt-title" data-l10n-id="printpreview-custom-scale-prompt-title"/>
+ `;
+ }
+ constructor() {
+ super();
+ this.disconnectedCallback = this.disconnectedCallback.bind(this);
+ }
+ connectedCallback() {
+ window.addEventListener("unload", this.disconnectedCallback, {
+ once: true,
+ });
+
+ MozXULElement.insertFTLIfNeeded("toolkit/printing/printPreview.ftl");
+ this.appendChild(this.constructor.fragment);
+
+ this.mPrintButton = document.getElementById("print-preview-print");
+
+ this.mPageSetupButton = document.getElementById(
+ "print-preview-pageSetup"
+ );
+
+ this.mNavigateHomeButton = document.getElementById(
+ "print-preview-navigateHome"
+ );
+
+ this.mNavigatePreviousButton = document.getElementById(
+ "print-preview-navigatePrevious"
+ );
+
+ this.mPageTextBox = document.getElementById("print-preview-pageNumber");
+
+ this.mNavigateNextButton = document.getElementById(
+ "print-preview-navigateNext"
+ );
+
+ this.mNavigateEndButton = document.getElementById(
+ "print-preview-navigateEnd"
+ );
+
+ this.mTotalPages = document.getElementById("print-preview-totalPages");
+
+ this.mScaleCombobox = document.getElementById("print-preview-scale");
+
+ this.mPortaitButton = document.getElementById(
+ "print-preview-portrait-button"
+ );
+
+ this.mLandscapeButton = document.getElementById(
+ "print-preview-landscape-button"
+ );
+
+ this.mSimplifyPageCheckbox = document.getElementById(
+ "print-preview-simplify"
+ );
+
+ this.mSimplifyPageNotAllowed = this.mSimplifyPageCheckbox.disabled;
+
+ this.mSimplifyPageToolbarSeparator = this.mSimplifyPageCheckbox.nextElementSibling;
+
+ this.mPrintPreviewObs = "";
+
+ this.mWebProgress = "";
+
+ this.mPPBrowser = null;
+
+ this.mOnPageTextBoxChange = () => {
+ this.navigate(0, Number(this.mPageTextBox.value), 0);
+ };
+ this.mPageTextBox.addEventListener("change", this.mOnPageTextBoxChange);
+
+ let dropdown = document.getElementById("print-preview-scale").menupopup;
+ for (let menuitem of dropdown.children) {
+ let value = menuitem.getAttribute("value");
+ if (!isNaN(parseFloat(value))) {
+ document.l10n.setAttributes(
+ menuitem,
+ "printpreview-percentage-value",
+ { percent: Math.round(parseFloat(value) * 100) }
+ );
+ }
+ }
+ }
+
+ initialize(aPPBrowser) {
+ let { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+ if (!Services.prefs.getBoolPref("print.use_simplify_page")) {
+ this.mSimplifyPageCheckbox.hidden = true;
+ this.mSimplifyPageToolbarSeparator.hidden = true;
+ }
+ this.mPPBrowser = aPPBrowser;
+ this.updateToolbar();
+
+ let ltr = document.documentElement.matches(":root:-moz-locale-dir(ltr)");
+ // Windows 7 doesn't support ⏮ and ⏭ by default, and fallback doesn't
+ // always work (bug 1343330).
+ let { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+ );
+ let useCompatCharacters = AppConstants.isPlatformAndVersionAtMost(
+ "win",
+ "6.1"
+ );
+ let leftEnd = useCompatCharacters ? "\u23EA" : "\u23EE";
+ let rightEnd = useCompatCharacters ? "\u23E9" : "\u23ED";
+ document.l10n.setAttributes(
+ this.mNavigateHomeButton,
+ "printpreview-homearrow",
+ { arrow: ltr ? leftEnd : rightEnd }
+ );
+ document.l10n.setAttributes(
+ this.mNavigatePreviousButton,
+ "printpreview-previousarrow",
+ { arrow: ltr ? "\u25C2" : "\u25B8" }
+ );
+ document.l10n.setAttributes(
+ this.mNavigateNextButton,
+ "printpreview-nextarrow",
+ { arrow: ltr ? "\u25B8" : "\u25C2" }
+ );
+ document.l10n.setAttributes(
+ this.mNavigateEndButton,
+ "printpreview-endarrow",
+ { arrow: ltr ? rightEnd : leftEnd }
+ );
+ }
+
+ destroy() {
+ delete this.mPPBrowser;
+ }
+
+ disconnectedCallback() {
+ window.removeEventListener("unload", this.disconnectedCallback);
+ this.mPageTextBox.removeEventListener(
+ "change",
+ this.mOnPageTextBoxChange
+ );
+ this.destroy();
+ }
+
+ disableUpdateTriggers(aDisabled) {
+ this.mPrintButton.disabled = aDisabled;
+ this.mPageSetupButton.disabled = aDisabled;
+ this.mNavigateHomeButton.disabled = aDisabled;
+ this.mNavigatePreviousButton.disabled = aDisabled;
+ this.mPageTextBox.disabled = aDisabled;
+ this.mNavigateNextButton.disabled = aDisabled;
+ this.mNavigateEndButton.disabled = aDisabled;
+ this.mScaleCombobox.disabled = aDisabled;
+ this.mPortaitButton.disabled = aDisabled;
+ this.mLandscapeButton.disabled = aDisabled;
+ this.mSimplifyPageCheckbox.disabled =
+ this.mSimplifyPageNotAllowed || aDisabled;
+ }
+
+ doPageSetup() {
+ /* import-globals-from printUtils.js */
+ var didOK = PrintUtils.showPageSetup();
+ if (didOK) {
+ // the changes that effect the UI
+ this.updateToolbar();
+
+ // Now do PrintPreview
+ PrintUtils.printPreview();
+ }
+ }
+
+ navigate(aDirection, aPageNum, aHomeOrEnd) {
+ const nsIWebBrowserPrint = Ci.nsIWebBrowserPrint;
+ let navType, pageNum;
+
+ let { min: lowerLimit, max: upperLimit } = this.mPageTextBox;
+
+ // 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") {
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_HOME;
+ this.mPageTextBox.value = 1;
+ } else {
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_END;
+ this.mPageTextBox.value = upperLimit;
+ }
+ pageNum = 0;
+ } else if (aDirection) {
+ // aDirection is either +1 or -1, and allows us to increment
+ // or decrement our currently viewed page.
+ pageNum = Number(this.mPageTextBox.value) + aDirection;
+ pageNum = Math.min(upperLimit, Math.max(lowerLimit, pageNum));
+ this.mPageTextBox.value = pageNum;
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM;
+ } else {
+ // We're going to a specific page (aPageNum)
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM;
+ pageNum = Math.min(upperLimit, Math.max(lowerLimit, aPageNum));
+ if (pageNum != aPageNum) {
+ this.mPageTextBox.value = pageNum;
+ }
+ }
+
+ this.mPPBrowser.sendMessageToActor(
+ "Printing:Preview:Navigate",
+ {
+ navType,
+ pageNum,
+ },
+ "Printing"
+ );
+ }
+
+ print() {
+ PrintUtils.printWindow(this.mPPBrowser.browsingContext);
+ }
+
+ promptForScaleValue(aValue) {
+ var value = Math.round(aValue);
+ var promptStr = document.getElementById("print-preview-scale-label")
+ .value;
+ var renameTitle = document.getElementById(
+ "print-preview-custom-scale-prompt-title"
+ ).textContent;
+ var result = { value };
+ let { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+ var confirmed = Services.prompt.prompt(
+ window,
+ renameTitle,
+ promptStr,
+ result,
+ null,
+ { value }
+ );
+ if (!confirmed || !result.value || result.value == "") {
+ return -1;
+ }
+ return result.value;
+ }
+
+ setScaleCombobox(aValue) {
+ var scaleValues = [
+ 0.3,
+ 0.4,
+ 0.5,
+ 0.6,
+ 0.7,
+ 0.8,
+ 0.9,
+ 1,
+ 1.25,
+ 1.5,
+ 1.75,
+ 2,
+ ];
+
+ aValue = Number(aValue);
+
+ for (var i = 0; i < scaleValues.length; i++) {
+ if (aValue == scaleValues[i]) {
+ this.mScaleCombobox.selectedIndex = i;
+ return;
+ }
+ }
+ this.mScaleCombobox.value = "Custom";
+ }
+
+ scale(aValue) {
+ var settings = PrintUtils.getPrintSettings();
+ if (aValue == "ShrinkToFit") {
+ if (!settings.shrinkToFit) {
+ settings.shrinkToFit = true;
+ this.savePrintSettings(
+ settings,
+ settings.kInitSaveShrinkToFit | settings.kInitSaveScaling
+ );
+ PrintUtils.printPreview();
+ }
+ return;
+ }
+
+ if (aValue == "Custom") {
+ aValue = this.promptForScaleValue(settings.scaling * 100.0);
+ if (aValue >= 10) {
+ aValue /= 100.0;
+ } else {
+ if (this.mScaleCombobox.hasAttribute("lastValidInx")) {
+ this.mScaleCombobox.selectedIndex = this.mScaleCombobox.getAttribute(
+ "lastValidInx"
+ );
+ }
+ return;
+ }
+ }
+
+ this.setScaleCombobox(aValue);
+ this.mScaleCombobox.setAttribute(
+ "lastValidInx",
+ this.mScaleCombobox.selectedIndex
+ );
+
+ if (settings.scaling != aValue || settings.shrinkToFit) {
+ settings.shrinkToFit = false;
+ settings.scaling = aValue;
+ this.savePrintSettings(
+ settings,
+ settings.kInitSaveShrinkToFit | settings.kInitSaveScaling
+ );
+ PrintUtils.printPreview();
+ }
+ }
+
+ orient(aOrientation) {
+ const kIPrintSettings = Ci.nsIPrintSettings;
+ var orientValue =
+ aOrientation == "portrait"
+ ? kIPrintSettings.kPortraitOrientation
+ : kIPrintSettings.kLandscapeOrientation;
+ var settings = PrintUtils.getPrintSettings();
+ if (settings.orientation != orientValue) {
+ settings.orientation = orientValue;
+ this.savePrintSettings(settings, settings.kInitSaveOrientation);
+ PrintUtils.printPreview();
+ }
+ }
+
+ simplify() {
+ PrintUtils.setSimplifiedMode(this.mSimplifyPageCheckbox.checked);
+ PrintUtils.printPreview();
+ }
+
+ enableSimplifyPage() {
+ this.mSimplifyPageNotAllowed = false;
+ this.mSimplifyPageCheckbox.disabled = false;
+ document.l10n.setAttributes(
+ this.mSimplifyPageCheckbox,
+ "printpreview-simplify-page-checkbox-enabled"
+ );
+ }
+
+ disableSimplifyPage() {
+ this.mSimplifyPageNotAllowed = true;
+ this.mSimplifyPageCheckbox.disabled = true;
+ document.l10n.setAttributes(
+ this.mSimplifyPageCheckbox,
+ "printpreview-simplify-page-checkbox"
+ );
+ }
+
+ updateToolbar() {
+ var settings = PrintUtils.getPrintSettings();
+
+ var isPortrait =
+ settings.orientation == Ci.nsIPrintSettings.kPortraitOrientation;
+
+ this.mPortaitButton.checked = isPortrait;
+ this.mLandscapeButton.checked = !isPortrait;
+
+ if (settings.shrinkToFit) {
+ this.mScaleCombobox.value = "ShrinkToFit";
+ } else {
+ this.setScaleCombobox(settings.scaling);
+ }
+
+ this.mPageTextBox.value = 1;
+ }
+
+ savePrintSettings(settings, flags) {
+ var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+ PSSVC.savePrintSettingsToPrefs(settings, true, flags);
+ }
+
+ updatePageCount(totalPages) {
+ this.mTotalPages.value = totalPages;
+ this.mPageTextBox.max = totalPages;
+ }
+ },
+ { extends: "toolbar" }
+);
diff --git a/toolkit/components/printing/content/printProgress.js b/toolkit/components/printing/content/printProgress.js
new file mode 100644
index 0000000000..0b91f9c3c4
--- /dev/null
+++ b/toolkit/components/printing/content/printProgress.js
@@ -0,0 +1,230 @@
+// -*- 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/. */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// dialog is just an array we'll use to store various properties from the dialog document...
+var dialog;
+
+// the printProgress is a nsIPrintProgress object
+var printProgress = null;
+
+// random global variables...
+var targetFile;
+
+var docTitle = "";
+var docURL = "";
+var progressParams = null;
+var switchUI = true;
+
+function ellipseString(aStr, doFront) {
+ if (!aStr) {
+ return "";
+ }
+
+ if (
+ aStr.length > 3 &&
+ (aStr.substr(0, 3) == "..." || aStr.substr(aStr.length - 4, 3) == "...")
+ ) {
+ return aStr;
+ }
+
+ var fixedLen = 64;
+ if (aStr.length > fixedLen) {
+ if (doFront) {
+ var endStr = aStr.substr(aStr.length - fixedLen, fixedLen);
+ return "..." + endStr;
+ }
+ var frontStr = aStr.substr(0, fixedLen);
+ return frontStr + "...";
+ }
+ return aStr;
+}
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ // Put progress meter in undetermined mode.
+ dialog.progress.removeAttribute("value");
+ }
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ // we are done printing
+ // Indicate completion in title area.
+ document.l10n.setAttributes(dialog.title, "print-complete");
+
+ // Put progress meter at 100%.
+ dialog.progress.setAttribute("value", 100);
+ document.l10n.setAttributes(dialog.progressText, "print-percent", {
+ percent: 100,
+ });
+
+ if (Services.focus.activeWindow == window) {
+ // This progress dialog is the currently active window. In
+ // this case we need to make sure that some other window
+ // gets focus before we close this dialog to work around the
+ // buggy Windows XP Fax dialog, which ends up parenting
+ // itself to the currently focused window and is unable to
+ // survive that window going away. What happens without this
+ // opener.focus() call on Windows XP is that the fax dialog
+ // is opened only to go away when this dialog actually
+ // closes (which can happen asynchronously, so the fax
+ // dialog just flashes once and then goes away), so w/o this
+ // fix, it's impossible to fax on Windows XP w/o manually
+ // switching focus to another window (or holding on to the
+ // progress dialog with the mouse long enough).
+ opener.focus();
+ }
+
+ window.close();
+ }
+ },
+
+ onProgressChange(
+ aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress
+ ) {
+ if (switchUI) {
+ dialog.tempLabel.setAttribute("hidden", "true");
+ dialog.progressBox.removeAttribute("hidden");
+
+ switchUI = false;
+ }
+
+ if (progressParams) {
+ var docTitleStr = ellipseString(progressParams.docTitle, false);
+ if (docTitleStr != docTitle) {
+ docTitle = docTitleStr;
+ dialog.title.value = docTitle;
+ }
+ var docURLStr = progressParams.docURL;
+ if (docURLStr != docURL && dialog.title != null) {
+ docURL = docURLStr;
+ if (docTitle == "") {
+ dialog.title.value = ellipseString(docURLStr, true);
+ }
+ }
+ }
+
+ // Calculate percentage.
+ var percent;
+ if (aMaxTotalProgress > 0) {
+ percent = Math.round((aCurTotalProgress * 100) / aMaxTotalProgress);
+ if (percent > 100) {
+ percent = 100;
+ }
+
+ // Advance progress meter.
+ dialog.progress.setAttribute("value", percent);
+
+ // Update percentage label on progress meter.
+ document.l10n.setAttributes(dialog.progressText, "print-percent", {
+ percent,
+ });
+ } else {
+ // Progress meter should be barber-pole in this case.
+ dialog.progress.removeAttribute("value");
+ // Update percentage label on progress meter.
+ dialog.progressText.setAttribute("value", "");
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // we can ignore this notification
+ },
+
+ onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ if (aMessage != "") {
+ dialog.title.setAttribute("value", aMessage);
+ }
+ },
+
+ onSecurityChange(aWebProgress, aRequest, state) {
+ // we can ignore this notification
+ },
+
+ onContentBlockingEvent(aWebProgress, aRequest, event) {
+ // we can ignore this notification
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+function loadDialog() {}
+
+function onLoad() {
+ // Set global variables.
+ printProgress = window.arguments[0];
+ if (window.arguments[1]) {
+ progressParams = window.arguments[1].QueryInterface(
+ Ci.nsIPrintProgressParams
+ );
+ if (progressParams) {
+ docTitle = ellipseString(progressParams.docTitle, false);
+ docURL = ellipseString(progressParams.docURL, true);
+ }
+ }
+
+ if (!printProgress) {
+ dump("Invalid argument to printProgress.xhtml\n");
+ window.close();
+ return;
+ }
+
+ dialog = {};
+ dialog.strings = [];
+ dialog.title = document.getElementById("dialog.title");
+ dialog.titleLabel = document.getElementById("dialog.titleLabel");
+ dialog.progress = document.getElementById("dialog.progress");
+ dialog.progressBox = document.getElementById("dialog.progressBox");
+ dialog.progressText = document.getElementById("dialog.progressText");
+ dialog.progressLabel = document.getElementById("dialog.progressLabel");
+ dialog.tempLabel = document.getElementById("dialog.tempLabel");
+
+ dialog.progressBox.setAttribute("hidden", "true");
+
+ document.l10n.setAttributes(dialog.tempLabel, "print-preparing");
+
+ dialog.title.value = docTitle;
+
+ // Fill dialog.
+ loadDialog();
+
+ document.addEventListener("dialogcancel", onCancel);
+ // set our web progress listener on the helper app launcher
+ printProgress.registerListener(progressListener);
+ // We need to delay the set title else dom will overwrite it
+ window.setTimeout(doneIniting, 500);
+}
+
+function onUnload() {
+ if (printProgress) {
+ try {
+ printProgress.unregisterListener(progressListener);
+ printProgress = null;
+ } catch (exception) {}
+ }
+}
+
+// If the user presses cancel, tell the app launcher and close the dialog.
+function onCancel() {
+ // Cancel app launcher.
+ try {
+ printProgress.processCanceledByUser = true;
+ } catch (exception) {}
+}
+
+function doneIniting() {
+ printProgress.doneIniting();
+}
diff --git a/toolkit/components/printing/content/printProgress.xhtml b/toolkit/components/printing/content/printProgress.xhtml
new file mode 100644
index 0000000000..9313fda7ab
--- /dev/null
+++ b/toolkit/components/printing/content/printProgress.xhtml
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+
+<!-- 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"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="print-window"
+ style="width: 36em;"
+ onload="onLoad()"
+ onunload="onUnload()">
+<dialog buttons="cancel">
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/printing/printDialogs.ftl"/>
+ </linkset>
+
+ <script src="chrome://global/content/printProgress.js"/>
+
+ <box style="display: grid; grid-template-columns: auto 1fr auto;" flex="1">
+ <!-- First row -->
+ <hbox pack="end">
+ <label id="dialog.titleLabel" data-l10n-id="print-title"/>
+ </hbox>
+ <label id="dialog.title"/>
+ <spacer/>
+
+ <!-- Second row -->
+ <hbox pack="end">
+ <html:label id="dialog.progressLabel" for="dialog.progress"
+ style="margin-right: 1em;" data-l10n-id="progress"/>
+ </hbox>
+ <box>
+ <label id="dialog.tempLabel" data-l10n-id="print-preparing"/>
+ <vbox pack="center" id="dialog.progressBox" flex="1">
+ <html:progress id="dialog.progress" value="0" max="100" style="width: 100%;"/>
+ </vbox>
+ </box>
+ <hbox pack="end" style="min-width: 2.5em;">
+ <label id="dialog.progressText"/>
+ </hbox>
+ </box>
+</dialog>
+</window>
diff --git a/toolkit/components/printing/content/printUtils.js b/toolkit/components/printing/content/printUtils.js
new file mode 100644
index 0000000000..2ecea1c8e0
--- /dev/null
+++ b/toolkit/components/printing/content/printUtils.js
@@ -0,0 +1,1073 @@
+// 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,
+ "PRINT_TAB_MODAL",
+ "print.tab_modal.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "PRINT_ALWAYS_SILENT",
+ "print.always_print_silent",
+ false
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PromptUtils",
+ "resource://gre/modules/SharedPromptUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PrintingParent",
+ "resource://gre/actors/PrintingParent.jsm"
+);
+
+var gFocusedElement = null;
+
+var gPendingPrintPreviews = new Map();
+
+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"
+ ));
+ },
+
+ /**
+ * 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.savePrintSettingsToPrefs(
+ printSettings,
+ false,
+ Ci.nsIPrintSettings.kInitSavePrinterName
+ );
+ PSSVC.savePrintSettingsToPrefs(
+ printSettings,
+ true,
+ Ci.nsIPrintSettings.kInitSaveAll
+ );
+ }
+ }
+ try {
+ var PRINTPROMPTSVC = Cc[
+ "@mozilla.org/embedcomp/printingprompt-service;1"
+ ].getService(Ci.nsIPrintingPromptService);
+ PRINTPROMPTSVC.showPageSetupDialog(window, printSettings, null);
+ } catch (e) {
+ dump("showPageSetup " + e + "\n");
+ return false;
+ }
+ return true;
+ },
+
+ getPreviewBrowser(sourceBrowser) {
+ let dialogBox = gBrowser.getTabDialogBox(sourceBrowser);
+ for (let dialog of dialogBox.getTabDialogManager()._dialogs) {
+ let browser = dialog._box.querySelector(".printPreviewBrowser");
+ if (browser) {
+ return browser;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Updates the hidden state of the "Print preview" and "Page Setup"
+ * menu items in the file menu depending on the print tab modal pref.
+ * The print preview menu item is not available on mac.
+ */
+ updatePrintPreviewMenuHiddenState() {
+ let printPreviewMenuItem = document.getElementById("menu_printPreview");
+ if (printPreviewMenuItem) {
+ printPreviewMenuItem.hidden = PRINT_TAB_MODAL;
+ }
+ let pageSetupMenuItem = document.getElementById("menu_printSetup");
+ if (pageSetupMenuItem) {
+ pageSetupMenuItem.hidden = PRINT_TAB_MODAL;
+ }
+ },
+
+ createPreviewBrowsers(aBrowsingContext, aDialogBrowser) {
+ let _createPreviewBrowser = previewType => {
+ // When we're not previewing the selection we want to make
+ // sure that the top-level browser is being printed.
+ let browsingContext =
+ previewType == "selection"
+ ? aBrowsingContext
+ : aBrowsingContext.top.embedderElement.browsingContext;
+ let browser = gBrowser.createBrowser({
+ remoteType: browsingContext.currentRemoteType,
+ userContextId: browsingContext.originAttributes.userContextId,
+ initialBrowsingContextGroupId: browsingContext.group.id,
+ skipLoad: true,
+ initiallyActive: true,
+ });
+ browser.addEventListener("DOMWindowClose", function(e) {
+ // Ignore close events from printing, see the code creating browsers in
+ // printUtils.js and nsDocumentViewer::OnDonePrinting.
+ //
+ // When we print with the new print UI we don't bother creating a new
+ // <browser> element, so the close event gets dispatched to us.
+ //
+ // Ignoring it is harmless (and doesn't cause correctness issues, because
+ // the preview document can't run script anyways).
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ browser.addEventListener("contextmenu", function(e) {
+ e.preventDefault();
+ });
+ browser.classList.add("printPreviewBrowser");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("printpreview", "true");
+ browser.setAttribute("previewtype", previewType);
+ document.l10n.setAttributes(browser, "printui-preview-label");
+ return browser;
+ };
+
+ let previewStack = document.importNode(
+ document.getElementById("printPreviewStackTemplate").content,
+ true
+ ).firstElementChild;
+
+ let previewBrowser = _createPreviewBrowser("primary");
+ previewStack.append(previewBrowser);
+ let selectionPreviewBrowser;
+ if (aBrowsingContext.currentRemoteType) {
+ selectionPreviewBrowser = _createPreviewBrowser("selection");
+ previewStack.append(selectionPreviewBrowser);
+ }
+
+ // show the toolbar after we go into print preview mode so
+ // that we can initialize the toolbar with total num pages
+ let previewPagination = document.createElement("printpreview-pagination");
+ previewPagination.classList.add("printPreviewNavigation");
+ previewStack.append(previewPagination);
+
+ aDialogBrowser.parentElement.prepend(previewStack);
+ return { previewBrowser, selectionPreviewBrowser };
+ },
+
+ /**
+ * 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.
+ */
+ async _openTabModalPrint(
+ aBrowsingContext,
+ aExistingPreviewBrowser,
+ aPrintInitiationTime,
+ aPrintSelectionOnly,
+ aPrintFrameOnly
+ ) {
+ let hasSelection = aPrintSelectionOnly;
+ if (!aPrintSelectionOnly) {
+ let sourceActor = aBrowsingContext.currentWindowGlobal.getActor(
+ "PrintingSelection"
+ );
+ hasSelection = await sourceActor.sendQuery(
+ "PrintingSelection:HasSelection"
+ );
+ }
+
+ 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...
+ if (aExistingPreviewBrowser) {
+ aExistingPreviewBrowser.remove();
+ }
+ return Promise.reject();
+ }
+
+ // Create a preview browser.
+ let args = PromptUtils.objectToPropBag({
+ previewBrowser: aExistingPreviewBrowser,
+ printSelectionOnly: !!aPrintSelectionOnly,
+ hasSelection,
+ printFrameOnly: !!aPrintFrameOnly,
+ });
+ let dialogBox = gBrowser.getTabDialogBox(sourceBrowser);
+ return dialogBox.open(
+ `chrome://global/content/print.html?browsingContextId=${aBrowsingContext.id}&printInitiationTime=${aPrintInitiationTime}`,
+ { features: "resizable=no", sizeTo: "available" },
+ args
+ );
+ },
+
+ /**
+ * Initialize a print, this will open the tab modal UI if it is enabled or
+ * defer to the native dialog/silent print.
+ *
+ * @param aTrigger What triggered the print, in string format, for telemetry
+ * purposes.
+ * @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
+ * {openWindowInfo} 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
+ * child process to clone into it.
+ * {printSelectionOnly} Whether to print only the active selection of
+ * the given browsing context.
+ * {printFrameOnly} Whether to print the selected frame.
+ */
+ startPrintWindow(aTrigger, aBrowsingContext, aOptions) {
+ const printInitiationTime = Date.now();
+ let openWindowInfo, printSelectionOnly, printFrameOnly;
+ if (aOptions) {
+ ({ openWindowInfo, printSelectionOnly, printFrameOnly } = aOptions);
+ }
+
+ // When we have a non-null openWindowInfo, we only want to record
+ // telemetry if we're triggered by window.print() itself, otherwise it's an
+ // internal print (like the one we do when we actually print from the
+ // preview dialog, etc.), and that'd cause us to overcount.
+ if (!openWindowInfo || openWindowInfo.isForWindowDotPrint) {
+ Services.telemetry.keyedScalarAdd("printing.trigger", aTrigger, 1);
+ }
+
+ let browser = null;
+ if (openWindowInfo) {
+ browser = document.createXULElement("browser");
+ browser.openWindowInfo = openWindowInfo;
+ browser.setAttribute("type", "content");
+ let remoteType = aBrowsingContext.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 (browser.isConnected) {
+ browser.remove();
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ });
+ browser.style.visibility = "collapse";
+ document.documentElement.appendChild(browser);
+ }
+
+ if (
+ PRINT_TAB_MODAL &&
+ !PRINT_ALWAYS_SILENT &&
+ (!openWindowInfo || openWindowInfo.isForWindowDotPrint)
+ ) {
+ let browsingContext = aBrowsingContext;
+ let focusedBc = Services.focus.focusedContentBrowsingContext;
+ if (
+ focusedBc &&
+ focusedBc.top.embedderElement == browsingContext.top.embedderElement &&
+ (!openWindowInfo || !openWindowInfo.isForWindowDotPrint) &&
+ !printFrameOnly
+ ) {
+ browsingContext = focusedBc;
+ }
+ this._openTabModalPrint(
+ browsingContext,
+ browser,
+ printInitiationTime,
+ printSelectionOnly,
+ printFrameOnly
+ ).catch(() => {});
+ return browser;
+ }
+
+ if (browser) {
+ // Legacy print dialog or silent printing, the content process will print
+ // in this <browser>.
+ return browser;
+ }
+
+ let settings = this.getPrintSettings();
+ settings.printSelectionOnly = printSelectionOnly;
+ this.printWindow(aBrowsingContext, settings);
+ return null;
+ },
+
+ /**
+ * Starts the process of printing the contents of a window.
+ *
+ * @param aBrowsingContext
+ * The BrowsingContext of the window to print.
+ * @param {Object?} aPrintSettings
+ * Optional print settings for the print operation
+ */
+ printWindow(aBrowsingContext, aPrintSettings) {
+ let windowID = aBrowsingContext.currentWindowGlobal.outerWindowId;
+ let topBrowser = aBrowsingContext.top.embedderElement;
+
+ const printPreviewIsOpen = !!document.getElementById(
+ "print-preview-toolbar"
+ );
+
+ if (printPreviewIsOpen) {
+ this._logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PREVIEW");
+ } else {
+ this._logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PAGE");
+ }
+
+ // Use the passed in settings if provided, otherwise pull the saved ones.
+ let printSettings = aPrintSettings || this.getPrintSettings();
+
+ // Set the title so that the print dialog can pick it up and
+ // use it to generate the filename for save-to-PDF.
+ printSettings.title = this._originalTitle || topBrowser.contentTitle;
+
+ if (this._shouldSimplify) {
+ // The generated document for simplified print preview has "about:blank"
+ // as its URL. We need to set docURL here so that the print header/footer
+ // can be given the original document's URL.
+ printSettings.docURL = this._originalURL || topBrowser.currentURI.spec;
+ }
+
+ // At some point we should handle the Promise that this returns (report
+ // rejection to telemetry?)
+ let promise = topBrowser.print(windowID, printSettings);
+
+ if (printPreviewIsOpen) {
+ if (this._shouldSimplify) {
+ this._logKeyedTelemetry("PRINT_COUNT", "SIMPLIFIED");
+ } else {
+ this._logKeyedTelemetry("PRINT_COUNT", "WITH_PREVIEW");
+ }
+ } else {
+ this._logKeyedTelemetry("PRINT_COUNT", "WITHOUT_PREVIEW");
+ }
+
+ return promise;
+ },
+
+ /**
+ * Initializes print preview.
+ *
+ * @param aTrigger Optionaly, if it's an external call, what triggered the
+ * print, in string format, for telemetry purposes.
+ * @param aListenerObj
+ * An object that defines the following functions:
+ *
+ * getPrintPreviewBrowser:
+ * Returns the <xul:browser> to display the print preview in. This
+ * <xul:browser> must have its type attribute set to "content".
+ *
+ * getSimplifiedPrintPreviewBrowser:
+ * Returns the <xul:browser> to display the simplified print preview
+ * in. This <xul:browser> must have its type attribute set to
+ * "content".
+ *
+ * getSourceBrowser:
+ * Returns the <xul:browser> that contains the document being
+ * printed. This <xul:browser> must have its type attribute set to
+ * "content".
+ *
+ * getSimplifiedSourceBrowser:
+ * Returns the <xul:browser> that contains the simplified version
+ * of the document being printed. This <xul:browser> must have its
+ * type attribute set to "content".
+ *
+ * getNavToolbox:
+ * Returns the primary toolbox for this window.
+ *
+ * onEnter:
+ * Called upon entering print preview.
+ *
+ * onExit:
+ * Called upon exiting print preview.
+ *
+ * These methods must be defined. printPreview can be called
+ * with aListenerObj as null iff this window is already displaying
+ * print preview (in which case, the previous aListenerObj passed
+ * to it will be used).
+ *
+ * Due to a timing issue resulting in a main-process crash, we have to
+ * manually open the progress dialog for print preview. The progress
+ * dialog is opened here in PrintUtils, and then we listen for update
+ * messages from the child. Bug 1558588 is about removing this.
+ */
+ printPreview(aTrigger, aListenerObj) {
+ if (aTrigger) {
+ Services.telemetry.keyedScalarAdd("printing.trigger", aTrigger, 1);
+ }
+
+ if (PRINT_TAB_MODAL) {
+ let browsingContext = gBrowser.selectedBrowser.browsingContext;
+ let focusedBc = Services.focus.focusedContentBrowsingContext;
+ if (
+ focusedBc &&
+ focusedBc.top.embedderElement == browsingContext.top.embedderElement
+ ) {
+ browsingContext = focusedBc;
+ }
+ return this._openTabModalPrint(
+ browsingContext,
+ /* aExistingPreviewBrowser = */ undefined,
+ Date.now()
+ );
+ }
+
+ // If we already have a toolbar someone is calling printPreview() to get us
+ // to refresh the display and aListenerObj won't be passed.
+ let printPreviewTB = document.getElementById("print-preview-toolbar");
+ if (!printPreviewTB) {
+ this._listener = aListenerObj;
+ this._sourceBrowser = aListenerObj.getSourceBrowser();
+ this._originalTitle = this._sourceBrowser.contentTitle;
+ this._originalURL = this._sourceBrowser.currentURI.spec;
+
+ // Here we log telemetry data for when the user enters print preview.
+ this.logTelemetry("PRINT_PREVIEW_OPENED_COUNT");
+ } else {
+ // Disable toolbar elements that can cause another update to be triggered
+ // during this update.
+ printPreviewTB.disableUpdateTriggers(true);
+
+ // collapse the browser here -- it will be shown in
+ // _enterPrintPreview; this forces a reflow which fixes display
+ // issues in bug 267422.
+ // We use the print preview browser as the source browser to avoid
+ // re-initializing print preview with a document that might now have changed.
+ this._sourceBrowser = this._shouldSimplify
+ ? this._listener.getSimplifiedPrintPreviewBrowser()
+ : this._listener.getPrintPreviewBrowser();
+ this._sourceBrowser.collapsed = true;
+
+ // If the user transits too quickly within preview and we have a pending
+ // progress dialog, we will close it before opening a new one.
+ this.ensureProgressDialogClosed();
+ }
+
+ this._webProgressPP = {};
+ let ppParams = {};
+ let notifyOnOpen = {};
+ let printSettings = this.getPrintSettings();
+ // Here we get the PrintingPromptService so we can display the PP Progress from script
+ // For the browser implemented via XUL with the PP toolbar we cannot let it be
+ // automatically opened from the print engine because the XUL scrollbars in the PP window
+ // will layout before the content window and a crash will occur.
+ // Doing it all from script, means it lays out before hand and we can let printing do its own thing
+ let PPROMPTSVC = Cc[
+ "@mozilla.org/embedcomp/printingprompt-service;1"
+ ].getService(Ci.nsIPrintingPromptService);
+
+ let promise = new Promise((resolve, reject) => {
+ this._onEntered.push({ resolve, reject });
+ });
+
+ // just in case we are already printing,
+ // an error code could be returned if the Progress Dialog is already displayed
+ try {
+ PPROMPTSVC.showPrintProgressDialog(
+ window,
+ printSettings,
+ this._obsPP,
+ false,
+ this._webProgressPP,
+ ppParams,
+ notifyOnOpen
+ );
+ if (ppParams.value) {
+ ppParams.value.docTitle = this._originalTitle;
+ ppParams.value.docURL = this._originalURL;
+ }
+
+ // this tells us whether we should continue on with PP or
+ // wait for the callback via the observer
+ if (!notifyOnOpen.value.valueOf() || this._webProgressPP.value == null) {
+ this._enterPrintPreview();
+ }
+ } catch (e) {
+ this._enterPrintPreview();
+ }
+ return promise;
+ },
+
+ // "private" methods and members. Don't use them.
+
+ _listener: null,
+ _closeHandlerPP: null,
+ _webProgressPP: null,
+ _sourceBrowser: null,
+ _originalTitle: "",
+ _originalURL: "",
+ _shouldSimplify: false,
+
+ _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) {
+ // 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.alert(window, title, msg);
+
+ Services.telemetry.keyedScalarAdd(
+ "printing.error",
+ this._getErrorCodeForNSResult(nsresult),
+ 1
+ );
+ },
+
+ _setPrinterDefaultsForSelectedPrinter(
+ aPSSVC,
+ aPrintSettings,
+ defaultsOnly = false
+ ) {
+ if (!aPrintSettings.printerName) {
+ aPrintSettings.printerName = aPSSVC.lastUsedPrinterName;
+ if (!aPrintSettings.printerName) {
+ // It is important to try to avoid passing settings over to the
+ // content process in the old print UI by saving to unprefixed prefs.
+ // To avoid that we try to get the name of a printer we can use.
+ let printerList = Cc["@mozilla.org/gfx/printerlist;1"].getService(
+ Ci.nsIPrinterList
+ );
+ aPrintSettings.printerName = printerList.systemDefaultPrinterName;
+ }
+ }
+
+ // 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 (aPrintSettings.printerName != this.SAVE_TO_PDF_PRINTER) {
+ aPSSVC.initPrintSettingsFromPrinter(
+ aPrintSettings.printerName,
+ aPrintSettings
+ );
+ }
+
+ if (!defaultsOnly) {
+ // now augment them with any values from last time
+ aPSSVC.initPrintSettingsFromPrefs(
+ aPrintSettings,
+ true,
+ aPrintSettings.kInitSaveAll
+ );
+ }
+ },
+
+ getPrintSettings(aPrinterName, defaultsOnly) {
+ var printSettings;
+ try {
+ var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+ printSettings = PSSVC.newPrintSettings;
+ if (aPrinterName) {
+ printSettings.printerName = aPrinterName;
+ }
+ this._setPrinterDefaultsForSelectedPrinter(
+ PSSVC,
+ printSettings,
+ defaultsOnly
+ );
+ } catch (e) {
+ dump("getPrintSettings: " + e + "\n");
+ }
+ return printSettings;
+ },
+
+ // This observer is called once the progress dialog has been "opened"
+ _obsPP: {
+ observe(aSubject, aTopic, aData) {
+ // Only process a null topic which means the progress dialog is open.
+ if (aTopic) {
+ return;
+ }
+
+ // delay the print preview to show the content of the progress dialog
+ setTimeout(function() {
+ PrintUtils._enterPrintPreview();
+ }, 0);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ },
+
+ get shouldSimplify() {
+ return this._shouldSimplify;
+ },
+
+ setSimplifiedMode(shouldSimplify) {
+ this._shouldSimplify = shouldSimplify;
+ },
+
+ _onEntered: [],
+
+ /**
+ * Currently, we create a new print preview browser to host the simplified
+ * cloned-document when Simplify Page option is used on preview. To accomplish
+ * this, we need to keep track of what browser should be presented, based on
+ * whether the 'Simplify page' checkbox is checked.
+ *
+ * _ppBrowsers
+ * Set of print preview browsers.
+ * _currentPPBrowser
+ * References the current print preview browser that is being presented.
+ */
+ _ppBrowsers: new Set(),
+ _currentPPBrowser: null,
+
+ _enterPrintPreview() {
+ // Send a message to the print preview browser to initialize
+ // print preview. If we happen to have gotten a print preview
+ // progress listener from nsIPrintingPromptService.showPrintProgressDialog
+ // in printPreview, we add listeners to feed that progress
+ // listener.
+ let ppBrowser = this._shouldSimplify
+ ? this._listener.getSimplifiedPrintPreviewBrowser()
+ : this._listener.getPrintPreviewBrowser();
+ this._ppBrowsers.add(ppBrowser);
+
+ // If we're switching from 'normal' print preview to 'simplified' print
+ // preview, we will want to run reader mode against the 'normal' print
+ // preview browser's content:
+ let oldPPBrowser = null;
+ let changingPrintPreviewBrowsers = false;
+ if (this._currentPPBrowser && ppBrowser != this._currentPPBrowser) {
+ changingPrintPreviewBrowsers = true;
+ oldPPBrowser = this._currentPPBrowser;
+ }
+ this._currentPPBrowser = ppBrowser;
+
+ let waitForPrintProgressToEnableToolbar = false;
+ if (this._webProgressPP.value) {
+ waitForPrintProgressToEnableToolbar = true;
+ }
+
+ gPendingPrintPreviews.set(ppBrowser, waitForPrintProgressToEnableToolbar);
+
+ // If we happen to have gotten simplify page checked, we will lazily
+ // instantiate a new tab that parses the original page using ReaderMode
+ // primitives. When it's ready, and in order to enter on preview, we send
+ // over a message to print preview browser passing up the simplified tab as
+ // reference. If not, we pass the original tab instead as content source.
+ if (this._shouldSimplify) {
+ let simplifiedBrowser = this._listener.getSimplifiedSourceBrowser();
+ if (!simplifiedBrowser) {
+ simplifiedBrowser = this._listener.createSimplifiedBrowser();
+
+ // Here, we send down a message to simplified browser in order to parse
+ // the original page. After we have parsed it, content will tell parent
+ // that the document is ready for print previewing.
+ simplifiedBrowser.sendMessageToActor(
+ "Printing:Preview:ParseDocument",
+ {
+ URL: this._originalURL,
+ windowID: oldPPBrowser.outerWindowID,
+ },
+ "Printing"
+ );
+
+ // Here we log telemetry data for when the user enters simplify mode.
+ this.logTelemetry("PRINT_PREVIEW_SIMPLIFY_PAGE_OPENED_COUNT");
+
+ return;
+ }
+ }
+
+ this.sendEnterPrintPreviewToChild(
+ ppBrowser,
+ this._sourceBrowser,
+ this._shouldSimplify,
+ changingPrintPreviewBrowsers
+ );
+ },
+
+ sendEnterPrintPreviewToChild(
+ ppBrowser,
+ sourceBrowser,
+ simplifiedMode,
+ changingBrowsers
+ ) {
+ ppBrowser.sendMessageToActor(
+ "Printing:Preview:Enter",
+ {
+ browsingContextId: sourceBrowser.browsingContext.id,
+ simplifiedMode,
+ changingBrowsers,
+ lastUsedPrinterName: this.getLastUsedPrinterName(),
+ },
+ "Printing"
+ );
+ },
+
+ printPreviewEntered(ppBrowser, previewResult) {
+ let waitForPrintProgressToEnableToolbar = gPendingPrintPreviews.get(
+ ppBrowser
+ );
+ gPendingPrintPreviews.delete(ppBrowser);
+
+ for (let { resolve, reject } of this._onEntered) {
+ if (previewResult.failed) {
+ reject();
+ } else {
+ resolve();
+ }
+ }
+
+ this._onEntered = [];
+ if (previewResult.failed) {
+ // Something went wrong while putting the document into print preview
+ // mode. Bail out.
+ this._ppBrowsers.clear();
+ this._listener.onEnter();
+ this._listener.onExit();
+ return;
+ }
+
+ // Stash the focused element so that we can return to it after exiting
+ // print preview.
+ gFocusedElement = document.commandDispatcher.focusedElement;
+
+ let printPreviewTB = document.getElementById("print-preview-toolbar");
+ if (printPreviewTB) {
+ if (previewResult.changingBrowsers) {
+ printPreviewTB.destroy();
+ printPreviewTB.initialize(ppBrowser);
+ } else {
+ // printPreviewTB.initialize above already calls updateToolbar.
+ printPreviewTB.updateToolbar();
+ }
+
+ // If we don't have a progress listener to enable the toolbar do it now.
+ if (!waitForPrintProgressToEnableToolbar) {
+ printPreviewTB.disableUpdateTriggers(false);
+ }
+
+ ppBrowser.collapsed = false;
+ ppBrowser.focus();
+ return;
+ }
+
+ // Set the original window as an active window so any mozPrintCallbacks can
+ // run without delayed setTimeouts.
+ if (this._listener.activateBrowser) {
+ this._listener.activateBrowser(this._sourceBrowser);
+ } else {
+ this._sourceBrowser.docShellIsActive = true;
+ }
+
+ // show the toolbar after we go into print preview mode so
+ // that we can initialize the toolbar with total num pages
+ printPreviewTB = document.createXULElement("toolbar", {
+ is: "printpreview-toolbar",
+ });
+ printPreviewTB.setAttribute("fullscreentoolbar", true);
+ printPreviewTB.setAttribute("flex", "1");
+ printPreviewTB.id = "print-preview-toolbar";
+
+ let navToolbox = this._listener.getNavToolbox();
+ navToolbox.parentNode.insertBefore(printPreviewTB, navToolbox);
+ printPreviewTB.initialize(ppBrowser);
+
+ // The print preview processing may not have fully completed, so if we
+ // have a progress listener, disable the toolbar elements that can trigger
+ // updates and it will enable them when completed.
+ if (waitForPrintProgressToEnableToolbar) {
+ printPreviewTB.disableUpdateTriggers(true);
+ }
+
+ // Enable simplify page checkbox when the page is an article
+ if (this._sourceBrowser.isArticle) {
+ printPreviewTB.enableSimplifyPage();
+ } else {
+ this.logTelemetry("PRINT_PREVIEW_SIMPLIFY_PAGE_UNAVAILABLE_COUNT");
+ printPreviewTB.disableSimplifyPage();
+ }
+
+ // copy the window close handler
+ if (window.onclose) {
+ this._closeHandlerPP = window.onclose;
+ } else {
+ this._closeHandlerPP = null;
+ }
+ window.onclose = function() {
+ PrintUtils.exitPrintPreview();
+ return false;
+ };
+
+ // disable chrome shortcuts...
+ window.addEventListener("keydown", this.onKeyDownPP, true);
+ window.addEventListener("keypress", this.onKeyPressPP, true);
+
+ ppBrowser.collapsed = false;
+ ppBrowser.focus();
+ // on Enter PP Call back
+ this._listener.onEnter();
+ },
+
+ readerModeReady(sourceBrowser) {
+ let ppBrowser = this._listener.getSimplifiedPrintPreviewBrowser();
+ this.sendEnterPrintPreviewToChild(ppBrowser, sourceBrowser, true, true);
+ },
+
+ getLastUsedPrinterName() {
+ let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+ let lastUsedPrinterName = PSSVC.lastUsedPrinterName;
+ if (!lastUsedPrinterName) {
+ // We "pass" print settings over to the content process by saving them to
+ // prefs (yuck!). It is important to try to avoid saving to prefs without
+ // prefixing them with a printer name though, so this hack tries to make
+ // sure that (in the common case) we have set the "last used" printer,
+ // which makes us save to prefs prefixed with its name, and makes sure
+ // the content process will pick settings up from those prefixed prefs
+ // too.
+ let settings = this.getPrintSettings();
+ if (settings.printerName) {
+ PSSVC.savePrintSettingsToPrefs(
+ settings,
+ false,
+ Ci.nsIPrintSettings.kInitSavePrinterName
+ );
+ PSSVC.savePrintSettingsToPrefs(
+ settings,
+ true,
+ Ci.nsIPrintSettings.kInitSaveAll
+ );
+ lastUsedPrinterName = settings.printerName;
+ }
+ }
+
+ return lastUsedPrinterName;
+ },
+
+ exitPrintPreview() {
+ for (let browser of this._ppBrowsers) {
+ browser.sendMessageToActor("Printing:Preview:Exit", {}, "Printing");
+ }
+ this._ppBrowsers.clear();
+ this._currentPPBrowser = null;
+ window.removeEventListener("keydown", this.onKeyDownPP, true);
+ window.removeEventListener("keypress", this.onKeyPressPP, true);
+
+ // restore the old close handler
+ if (this._closeHandlerPP) {
+ window.onclose = this._closeHandlerPP;
+ } else {
+ window.onclose = null;
+ }
+ this._closeHandlerPP = null;
+
+ // remove the print preview toolbar
+ let printPreviewTB = document.getElementById("print-preview-toolbar");
+ printPreviewTB.destroy();
+ printPreviewTB.remove();
+
+ if (gFocusedElement) {
+ Services.focus.setFocus(gFocusedElement, Services.focus.FLAG_NOSCROLL);
+ } else {
+ this._sourceBrowser.focus();
+ }
+ gFocusedElement = null;
+
+ this.setSimplifiedMode(false);
+
+ this.ensureProgressDialogClosed();
+
+ this._listener.onExit();
+
+ this._originalTitle = "";
+ this._originalURL = "";
+ },
+
+ logTelemetry(ID) {
+ let histogram = Services.telemetry.getHistogramById(ID);
+ histogram.add(true);
+ },
+
+ _logKeyedTelemetry(id, key) {
+ let histogram = Services.telemetry.getKeyedHistogramById(id);
+ histogram.add(key);
+ },
+
+ onKeyDownPP(aEvent) {
+ // Esc exits the PP
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ PrintUtils.exitPrintPreview();
+ }
+ },
+
+ onKeyPressPP(aEvent) {
+ var closeKey;
+ try {
+ closeKey = document.getElementById("key_close").getAttribute("key");
+ closeKey = aEvent["DOM_VK_" + closeKey];
+ } catch (e) {}
+ var isModif = aEvent.ctrlKey || aEvent.metaKey;
+ // Ctrl-W exits the PP
+ if (
+ isModif &&
+ (aEvent.charCode == closeKey || aEvent.charCode == closeKey + 32)
+ ) {
+ PrintUtils.exitPrintPreview();
+ } else if (isModif) {
+ var printPreviewTB = document.getElementById("print-preview-toolbar");
+ var printKey = document
+ .getElementById("printKb")
+ .getAttribute("key")
+ .toUpperCase();
+ var pressedKey = String.fromCharCode(aEvent.charCode).toUpperCase();
+ if (printKey == pressedKey) {
+ printPreviewTB.print();
+ }
+ }
+ // cancel shortkeys
+ if (isModif) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ },
+
+ /**
+ * If there's a printing or print preview progress dialog displayed, force
+ * it to close now.
+ */
+ ensureProgressDialogClosed() {
+ if (this._webProgressPP && this._webProgressPP.value) {
+ this._webProgressPP.value.onStateChange(
+ null,
+ null,
+ Ci.nsIWebProgressListener.STATE_STOP,
+ 0
+ );
+ }
+ },
+};
diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css
new file mode 100644
index 0000000000..257acad91b
--- /dev/null
+++ b/toolkit/components/printing/content/simplifyMode.css
@@ -0,0 +1,32 @@
+/* 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. */
+
+body {
+ padding-top: 0px;
+ padding-bottom: 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..fb28981a77
--- /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: 2px;
+ border-end-start-radius: 2px;
+}
+
+.toggle-group-label:last-of-type {
+ border-start-end-radius: 2px;
+ border-end-end-radius: 2px;
+}
+
+.toggle-group-input:not(:enabled) + .toggle-group-label {
+ opacity: 0.7;
+}
+
+.toggle-group-input:enabled + .toggle-group-label:hover {
+ background-color: var(--in-content-button-background-hover);
+}
+
+.toggle-group-input:enabled + .toggle-group-label:active {
+ background-color: var(--in-content-button-background-active);
+}
+
+.toggle-group-input:-moz-focusring + .toggle-group-label {
+ box-shadow: 0 0 0 1px var(--border-color), 0 0 0 4px rgba(10, 132, 255, 0.3);
+ outline: none;
+ z-index: 1;
+}
+
+.toggle-group-input:checked:-moz-focusring + .toggle-group-label {
+ box-shadow: 0 0 0 1px var(--in-content-border-active), 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
+
+.toggle-group-input:enabled:checked + .toggle-group-label{
+ background-color: var(--in-content-primary-button-background);
+ color: var(--in-content-selected-text);
+}
+
+.toggle-group-input:enabled:checked + .toggle-group-label:hover {
+ background-color: var(--in-content-primary-button-background-hover);
+}
+
+.toggle-group-input:enabled:checked + .toggle-group-label:active {
+ background-color: var(--in-content-primary-button-background-active);
+}