summaryrefslogtreecommitdiffstats
path: root/toolkit/components/printing
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/printing')
-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
-rw-r--r--toolkit/components/printing/jar.mn26
-rw-r--r--toolkit/components/printing/moz.build12
-rw-r--r--toolkit/components/printing/tests/.eslintrc.js5
-rw-r--r--toolkit/components/printing/tests/browser.ini66
-rw-r--r--toolkit/components/printing/tests/browser_cancel_close_print.js28
-rw-r--r--toolkit/components/printing/tests/browser_destination_change.js152
-rw-r--r--toolkit/components/printing/tests/browser_empty_paper_sizes.js97
-rw-r--r--toolkit/components/printing/tests/browser_modal_print.js247
-rw-r--r--toolkit/components/printing/tests/browser_modal_resize.js155
-rw-r--r--toolkit/components/printing/tests/browser_page_change_print_original.js88
-rw-r--r--toolkit/components/printing/tests/browser_pdf_hidden_settings.js39
-rw-r--r--toolkit/components/printing/tests/browser_pdf_printer_settings.js116
-rw-r--r--toolkit/components/printing/tests/browser_preview_in_container.js60
-rw-r--r--toolkit/components/printing/tests/browser_preview_navigation.js424
-rw-r--r--toolkit/components/printing/tests/browser_preview_print_coop.js40
-rw-r--r--toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js102
-rw-r--r--toolkit/components/printing/tests/browser_preview_switch_print_selected.js126
-rw-r--r--toolkit/components/printing/tests/browser_print_bcg_id_overflow.js61
-rw-r--r--toolkit/components/printing/tests/browser_print_context_menu.js68
-rw-r--r--toolkit/components/printing/tests/browser_print_copies.js41
-rw-r--r--toolkit/components/printing/tests/browser_print_duplex.js174
-rw-r--r--toolkit/components/printing/tests/browser_print_in_container.js38
-rw-r--r--toolkit/components/printing/tests/browser_print_margins.js802
-rw-r--r--toolkit/components/printing/tests/browser_print_page_range.js471
-rw-r--r--toolkit/components/printing/tests/browser_print_paper_sizes.js120
-rw-r--r--toolkit/components/printing/tests/browser_print_scaling.js46
-rw-r--r--toolkit/components/printing/tests/browser_print_selection.js160
-rw-r--r--toolkit/components/printing/tests/browser_sheet_count.js228
-rw-r--r--toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js105
-rw-r--r--toolkit/components/printing/tests/browser_ui_labels.js23
-rw-r--r--toolkit/components/printing/tests/browser_window_print.js189
-rw-r--r--toolkit/components/printing/tests/file_coop_header.html6
-rw-r--r--toolkit/components/printing/tests/file_coop_header.html^headers^1
-rw-r--r--toolkit/components/printing/tests/file_page_change_print_original_1.html8
-rw-r--r--toolkit/components/printing/tests/file_page_change_print_original_2.html1
-rw-r--r--toolkit/components/printing/tests/file_pdf.pdf12
-rw-r--r--toolkit/components/printing/tests/file_print.html5
-rw-r--r--toolkit/components/printing/tests/file_window_print.html29
-rw-r--r--toolkit/components/printing/tests/file_window_print_delayed_during_load.html13
-rw-r--r--toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html7
-rw-r--r--toolkit/components/printing/tests/head.js444
-rw-r--r--toolkit/components/printing/tests/longerArticle.html21
-rw-r--r--toolkit/components/printing/tests/simplifyArticleSample.html16
-rw-r--r--toolkit/components/printing/tests/simplifyNonArticleSample.html9
61 files changed, 11230 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);
+}
diff --git a/toolkit/components/printing/jar.mn b/toolkit/components/printing/jar.mn
new file mode 100644
index 0000000000..2183b5607c
--- /dev/null
+++ b/toolkit/components/printing/jar.mn
@@ -0,0 +1,26 @@
+# 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/.
+
+toolkit.jar:
+#ifndef XP_MACOSX
+#ifdef XP_WIN
+ content/global/printPageSetup.js (content/printPageSetup.js)
+ content/global/printPageSetup.xhtml (content/printPageSetup.xhtml)
+#endif
+ content/global/printPreviewProgress.js (content/printPreviewProgress.js)
+ content/global/printPreviewProgress.xhtml (content/printPreviewProgress.xhtml)
+ content/global/printProgress.js (content/printProgress.js)
+ content/global/printProgress.xhtml (content/printProgress.xhtml)
+#endif
+ content/global/landscape.svg (content/landscape.svg)
+ content/global/portrait.svg (content/portrait.svg)
+ content/global/printPreviewPagination.js (content/printPreviewPagination.js)
+ content/global/printPreviewToolbar.js (content/printPreviewToolbar.js)
+ content/global/printUtils.js (content/printUtils.js)
+ content/global/print.js (content/print.js)
+ content/global/print.html (content/print.html)
+ content/global/print.css (content/print.css)
+ content/global/toggle-group.css (content/toggle-group.css)
+ content/global/simplifyMode.css (content/simplifyMode.css)
+ content/global/printPagination.css (content/printPagination.css)
diff --git a/toolkit/components/printing/moz.build b/toolkit/components/printing/moz.build
new file mode 100644
index 0000000000..33ce2241e4
--- /dev/null
+++ b/toolkit/components/printing/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Printing")
diff --git a/toolkit/components/printing/tests/.eslintrc.js b/toolkit/components/printing/tests/.eslintrc.js
new file mode 100644
index 0000000000..03ec23d8d0
--- /dev/null
+++ b/toolkit/components/printing/tests/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/chrome-test", "plugin:mozilla/browser-test"],
+};
diff --git a/toolkit/components/printing/tests/browser.ini b/toolkit/components/printing/tests/browser.ini
new file mode 100644
index 0000000000..8a1a61cc94
--- /dev/null
+++ b/toolkit/components/printing/tests/browser.ini
@@ -0,0 +1,66 @@
+[DEFAULT]
+support-files =
+ head.js
+ simplifyArticleSample.html
+
+[browser_cancel_close_print.js]
+[browser_destination_change.js]
+[browser_empty_paper_sizes.js]
+
+[browser_modal_print.js]
+
+[browser_modal_resize.js]
+
+[browser_page_change_print_original.js]
+support-files =
+ file_page_change_print_original_1.html
+ file_page_change_print_original_2.html
+skip-if = os == "mac"
+
+[browser_pdf_hidden_settings.js]
+support-files =
+ file_pdf.pdf
+[browser_print_copies.js]
+[browser_print_paper_sizes.js]
+[browser_pdf_printer_settings.js]
+[browser_print_bcg_id_overflow.js]
+[browser_print_context_menu.js]
+[browser_print_duplex.js]
+skip-if = (verify && (os == 'mac')) # bug 1675609
+[browser_print_margins.js]
+[browser_print_selection.js]
+[browser_print_page_range.js]
+[browser_print_scaling.js]
+[browser_sheet_count.js]
+[browser_ui_labels.js]
+[browser_window_print.js]
+support-files =
+ file_window_print.html
+ file_window_print_delayed_during_load.html
+ file_window_print_sandboxed_iframe.html
+
+[browser_preview_in_container.js]
+support-files =
+ file_print.html
+
+[browser_preview_navigation.js]
+support-files =
+ longerArticle.html
+
+[browser_preview_print_simplify_non_article.js]
+support-files =
+ simplifyNonArticleSample.html
+skip-if = os == "mac" || (verify && (os == 'win' || os == 'linux'))
+
+[browser_preview_print_coop.js]
+support-files =
+ file_coop_header.html
+ file_coop_header.html^headers^
+
+[browser_preview_switch_print_selected.js]
+skip-if = os == "mac" || (verify && !debug && (os == 'linux'))
+
+[browser_print_in_container.js]
+skip-if = tsan # Bug 1683730
+
+[browser_system_dialog_subdialog_hidden.js]
diff --git a/toolkit/components/printing/tests/browser_cancel_close_print.js b/toolkit/components/printing/tests/browser_cancel_close_print.js
new file mode 100644
index 0000000000..7fc311a33f
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_cancel_close_print.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testCloseWhilePrinting() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.setupMockPrint();
+ helper.mockFilePicker("output.pdf");
+
+ await helper.withClosingFn(async () => {
+ let cancelButton = helper.get("cancel-button");
+ is(
+ helper.doc.l10n.getAttributes(cancelButton).id,
+ "printui-cancel-button",
+ "The cancel button is using the 'cancel' string"
+ );
+ EventUtils.sendKey("return", helper.win);
+ is(
+ helper.doc.l10n.getAttributes(cancelButton).id,
+ "printui-close-button",
+ "The cancel button is using the 'close' string"
+ );
+ helper.resolvePrint();
+ });
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_destination_change.js b/toolkit/components/printing/tests/browser_destination_change.js
new file mode 100644
index 0000000000..a9f1dd0f82
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_destination_change.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let pdfPrinterName = "Mozilla Save to PDF";
+let fastPrinterName = "Fast";
+let slowPrinterName = "Slow";
+
+async function setupPrinters(helper) {
+ helper.addMockPrinter(fastPrinterName);
+
+ let resolvePrinterInfo;
+ helper.addMockPrinter({
+ name: slowPrinterName,
+ printerInfoPromise: new Promise(resolve => {
+ resolvePrinterInfo = resolve;
+ }),
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.printer_Slow.print_orientation", 1]],
+ });
+
+ return resolvePrinterInfo;
+}
+
+async function changeDestination(helper, dir) {
+ let picker = helper.get("printer-picker");
+ let changed = BrowserTestUtils.waitForEvent(picker, "change");
+ picker.focus();
+ EventUtils.sendKey("space", helper.win);
+ EventUtils.sendKey(dir, helper.win);
+ EventUtils.sendKey("return", helper.win);
+ await changed;
+}
+
+function assertFormEnabled(form) {
+ for (let element of form.elements) {
+ if (element.hasAttribute("disallowed")) {
+ ok(element.disabled, `${element.id} is disallowed`);
+ } else {
+ ok(!element.disabled, `${element.id} is enabled`);
+ }
+ }
+}
+
+function assertFormDisabled(form) {
+ for (let element of form.elements) {
+ if (element.id == "printer-picker" || element.id == "cancel-button") {
+ ok(!element.disabled, `${element.id} is enabled`);
+ } else {
+ ok(element.disabled, `${element.id} is disabled`);
+ }
+ }
+}
+
+add_task(async function testSlowDestinationChange() {
+ await PrintHelper.withTestPage(async helper => {
+ let resolvePrinterInfo = await setupPrinters(helper);
+ await helper.startPrint();
+
+ let destinationPicker = helper.get("printer-picker");
+ let printForm = helper.get("print");
+
+ info("Changing to fast printer should change settings");
+ await helper.assertSettingsChanged(
+ { printerName: pdfPrinterName, orientation: 0 },
+ { printerName: fastPrinterName, orientation: 0 },
+ async () => {
+ await changeDestination(helper, "down");
+ is(destinationPicker.value, fastPrinterName, "Fast printer selected");
+ // Wait one frame so the print settings promises resolve.
+ await helper.awaitAnimationFrame();
+ assertFormEnabled(printForm);
+ }
+ );
+
+ info("Changing to slow printer should not change settings yet");
+ await helper.assertSettingsNotChanged(
+ { printerName: fastPrinterName, orientation: 0 },
+ async () => {
+ await changeDestination(helper, "down");
+ is(destinationPicker.value, slowPrinterName, "Slow printer selected");
+ // Wait one frame, since the settings are blocked on resolvePrinterInfo
+ // the settings shouldn't change.
+ await helper.awaitAnimationFrame();
+ assertFormDisabled(printForm);
+ }
+ );
+
+ await helper.assertSettingsChanged(
+ { printerName: fastPrinterName, orientation: 0 },
+ { printerName: slowPrinterName, orientation: 1 },
+ async () => {
+ resolvePrinterInfo();
+ await helper.waitForSettingsEvent();
+ assertFormEnabled(printForm);
+ }
+ );
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testSwitchAwayFromSlowDestination() {
+ await PrintHelper.withTestPage(async helper => {
+ let resolvePrinterInfo = await setupPrinters(helper);
+ await helper.startPrint();
+
+ let destinationPicker = helper.get("printer-picker");
+ let printForm = helper.get("print");
+
+ // Load the fast printer.
+ await helper.waitForSettingsEvent(async () => {
+ await changeDestination(helper, "down");
+ });
+ await helper.awaitAnimationFrame();
+ assertFormEnabled(printForm);
+
+ // "Load" the slow printer.
+ await changeDestination(helper, "down");
+ is(destinationPicker.value, slowPrinterName, "Slow printer selected");
+ // Wait an animation frame, since there's no settings event.
+ await helper.awaitAnimationFrame();
+ assertFormDisabled(printForm);
+
+ // Switch back to the fast printer.
+ await helper.waitForSettingsEvent(async () => {
+ await changeDestination(helper, "up");
+ });
+ helper.assertSettingsMatch({
+ printerName: fastPrinterName,
+ orientation: 0,
+ });
+
+ await helper.awaitAnimationFrame();
+ assertFormEnabled(printForm);
+
+ // Let the slow printer settings resolve, the orientation shouldn't change.
+ resolvePrinterInfo();
+ // Wait so the settings event can trigger, if this case isn't handled.
+ await helper.awaitAnimationFrame();
+ helper.assertSettingsMatch({
+ printerName: fastPrinterName,
+ orientation: 0,
+ });
+ assertFormEnabled(printForm);
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_empty_paper_sizes.js b/toolkit/components/printing/tests/browser_empty_paper_sizes.js
new file mode 100644
index 0000000000..8fcf3c3c66
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_empty_paper_sizes.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testSanityCheckPaperList() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ let paperList = [
+ PrintHelper.createMockPaper({
+ id: "regular",
+ name: "Regular Paper",
+ }),
+ PrintHelper.createMockPaper({
+ id: "large",
+ name: "Large Size",
+ width: 720,
+ height: 1224,
+ }),
+ ];
+ helper.addMockPrinter({ name: mockPrinterName, paperList });
+ await helper.startPrint();
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await helper.awaitAnimationFrame();
+
+ is(
+ helper.settings.printerName,
+ mockPrinterName,
+ "The Fake Printer is current printer"
+ );
+ is(
+ Object.values(helper.win.PrintSettingsViewProxy.availablePaperSizes)
+ .length,
+ 2,
+ "There are 2 paper sizes"
+ );
+ ok(
+ helper.win.PrintSettingsViewProxy.availablePaperSizes.regular,
+ "'regular' paper size is available"
+ );
+ ok(
+ helper.win.PrintSettingsViewProxy.availablePaperSizes.large,
+ "'large' paper size is available"
+ );
+ });
+});
+
+add_task(async function testEmptyPaperListGetsFallbackPaperSizes() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+
+ is(
+ Object.values(helper.win.PrintSettingsViewProxy.availablePrinters).length,
+ 2,
+ "There are 2 available printers"
+ );
+ ok(
+ helper.win.PrintSettingsViewProxy.availablePrinters[mockPrinterName],
+ "The Fake Printer is one of our availablePrinters"
+ );
+
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await helper.awaitAnimationFrame();
+
+ is(
+ helper.settings.printerName,
+ mockPrinterName,
+ "The Fake Printer is current printer"
+ );
+ is(
+ helper.get("printer-picker").value,
+ mockPrinterName,
+ "The Fake Printer is selected"
+ );
+
+ let printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
+ Ci.nsIPrinterList
+ );
+ let fallbackPaperList = await printerList.fallbackPaperList;
+ let paperPickerSizes = Array.from(
+ helper.get("paper-size-picker").options
+ ).map(o => o.value);
+ for (let paper of fallbackPaperList) {
+ ok(
+ helper.win.PrintSettingsViewProxy.availablePaperSizes[paper.id],
+ "Fallback paper size: " + paper.id + " is available"
+ );
+ ok(
+ paperPickerSizes.includes(paper.id),
+ "There is a paper size options for " + paper.id
+ );
+ }
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_modal_print.js b/toolkit/components/printing/tests/browser_modal_print.js
new file mode 100644
index 0000000000..a3cbf95d65
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_modal_print.js
@@ -0,0 +1,247 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function assertExpectedPrintPage(helper) {
+ is(
+ helper.sourceURI,
+ PrintHelper.defaultTestPageUrl,
+ "The URL of the browser is the one we expect"
+ );
+}
+
+add_task(async function testModalPrintDialog() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.assertDialogClosed();
+
+ await helper.startPrint();
+
+ helper.assertDialogOpen();
+
+ // Check that we're printing the right page.
+ assertExpectedPrintPage(helper);
+
+ // Close the dialog with Escape.
+ await helper.withClosingFn(() => {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win);
+ });
+
+ helper.assertDialogClosed();
+ });
+});
+
+add_task(async function testPrintMultiple() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.assertDialogClosed();
+
+ // First print as usual.
+ await helper.startPrint();
+ helper.assertDialogOpen();
+ assertExpectedPrintPage(helper);
+
+ // Trigger the command a few more times, verify the overlay still exists.
+ await helper.startPrint();
+ helper.assertDialogOpen();
+ await helper.startPrint();
+ helper.assertDialogOpen();
+ await helper.startPrint();
+ helper.assertDialogOpen();
+
+ // Verify it's still the correct page.
+ assertExpectedPrintPage(helper);
+
+ // Make sure we clean up, ideally this would be handled by the helper.
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testCancelButton() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.assertDialogClosed();
+ await helper.startPrint();
+ helper.assertDialogOpen();
+
+ let cancelButton = helper.doc.querySelector("button[name=cancel]");
+ ok(cancelButton, "Got the cancel button");
+ await helper.withClosingFn(() =>
+ EventUtils.synthesizeMouseAtCenter(cancelButton, {}, helper.win)
+ );
+
+ helper.assertDialogClosed();
+ });
+});
+
+add_task(async function testTabOrder() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.assertDialogClosed();
+ await helper.startPrint();
+ helper.assertDialogOpen();
+
+ const printerPicker = helper.doc.getElementById("printer-picker");
+ is(
+ helper.doc.activeElement,
+ printerPicker,
+ "Initial focus on printer picker"
+ );
+
+ const previewBrowser = document.querySelector(".printPreviewBrowser");
+ ok(previewBrowser, "Got the print preview browser");
+
+ let focused;
+ let navigationShadowRoot = document.querySelector(".printPreviewNavigation")
+ .shadowRoot;
+ for (let buttonId of [
+ "navigateEnd",
+ "navigateNext",
+ "navigatePrevious",
+ "navigateHome",
+ ]) {
+ let button = navigationShadowRoot.getElementById(buttonId);
+ focused = BrowserTestUtils.waitForEvent(button, "focus");
+ await EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await focused;
+ }
+
+ focused = BrowserTestUtils.waitForEvent(previewBrowser, "focus");
+ await EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await focused;
+ ok(true, "Print preview focused after shift+tab through the paginator");
+
+ focused = BrowserTestUtils.waitForEvent(gNavToolbox, "focus", true);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await focused;
+ ok(true, "Toolbox focused after shift+tab");
+
+ focused = BrowserTestUtils.waitForEvent(previewBrowser, "focus");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await focused;
+ ok(true, "Print preview focused after tab");
+
+ for (let buttonId of [
+ "navigateHome",
+ "navigatePrevious",
+ "navigateNext",
+ "navigateEnd",
+ ]) {
+ let button = navigationShadowRoot.getElementById(buttonId);
+ focused = BrowserTestUtils.waitForEvent(button, "focus");
+ await EventUtils.synthesizeKey("KEY_Tab");
+ await focused;
+ }
+ focused = BrowserTestUtils.waitForEvent(printerPicker, "focus");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await focused;
+ ok(true, "Printer picker focused after tab");
+
+ const lastButtonName = AppConstants.platform == "win" ? "cancel" : "print";
+ const lastButton = helper.doc.querySelector(
+ `button[name=${lastButtonName}]`
+ );
+ focused = BrowserTestUtils.waitForEvent(lastButton, "focus");
+ lastButton.focus();
+ await focused;
+ ok(true, "Last button focused");
+
+ focused = BrowserTestUtils.waitForEvent(gNavToolbox, "focus", true);
+ EventUtils.synthesizeKey("KEY_Tab");
+ await focused;
+ ok(true, "Toolbox focused after tab");
+
+ focused = BrowserTestUtils.waitForEvent(lastButton, "focus");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await focused;
+ ok(true, "Cancel button focused after shift+tab");
+
+ await helper.withClosingFn(() => {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ });
+
+ helper.assertDialogClosed();
+ });
+});
+
+async function testPrintWithEnter(testFn, filename) {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let file = helper.mockFilePicker(filename);
+ await testFn(helper);
+ await helper.assertPrintToFile(file, () => {
+ EventUtils.sendKey("return", helper.win);
+ const cancelButton = helper.doc.querySelector(`button[name="cancel"]`);
+ ok(!cancelButton.disabled, "Cancel button is not disabled");
+ const printButton = helper.doc.querySelector(`button[name="print"]`);
+ ok(printButton.disabled, "Print button is disabled");
+ });
+ });
+}
+
+add_task(async function testEnterAfterLoadPrints() {
+ info("Test print without moving focus");
+ await testPrintWithEnter(() => {}, "print_initial_focus.pdf");
+});
+
+add_task(async function testEnterPrintsFromPageRangeSelect() {
+ info("Test print from page range select");
+ await testPrintWithEnter(helper => {
+ let pageRangePicker = helper.get("range-picker");
+ pageRangePicker.focus();
+ is(
+ helper.doc.activeElement,
+ pageRangePicker,
+ "Page range select is focused"
+ );
+ }, "print_page_range_select.pdf");
+});
+
+add_task(async function testEnterPrintsFromOrientation() {
+ info("Test print on Enter from focused orientation input");
+ await testPrintWithEnter(helper => {
+ let portrait = helper.get("portrait");
+ portrait.focus();
+ is(helper.doc.activeElement, portrait, "Portrait is focused");
+ }, "print_orientation_focused.pdf");
+});
+
+add_task(async function testPrintOnNewWindowDoesntClose() {
+ info("Test that printing doesn't close a window with a single tab");
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ let file = helper.mockFilePicker("print_new_window_close.pdf");
+ await helper.assertPrintToFile(file, () => {
+ EventUtils.sendKey("return", helper.win);
+ });
+ });
+ ok(!win.closed, "Shouldn't be closed");
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testPrintProgressIndicator() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ helper.setupMockPrint();
+
+ let progressIndicator = helper.get("print-progress");
+ ok(progressIndicator.hidden, "Progress indicator is hidden");
+
+ let indicatorShown = BrowserTestUtils.waitForAttributeRemoval(
+ "hidden",
+ progressIndicator
+ );
+ helper.click(helper.get("print-button"));
+ await indicatorShown;
+
+ ok(!progressIndicator.hidden, "Progress indicator is shown on print start");
+
+ await helper.withClosingFn(async () => {
+ await helper.resolvePrint();
+ });
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_modal_resize.js b/toolkit/components/printing/tests/browser_modal_resize.js
new file mode 100644
index 0000000000..4c0ac31b18
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_modal_resize.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function waitForAnimationFrames() {
+ // Wait for 2 animation frames in hopes it's actually done resizing.
+ return new Promise(resolve =>
+ window.requestAnimationFrame(() => window.requestAnimationFrame(resolve))
+ );
+}
+
+async function mouseMoveAndWait(elem) {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(elem, "mousemove");
+ EventUtils.synthesizeMouseAtCenter(elem, { type: "mousemove" });
+ await mouseMovePromise;
+ await TestUtils.waitForTick();
+}
+
+function closeEnough(actual, expected) {
+ return expected - 1 < actual && actual < expected + 1;
+}
+
+async function resizeWindow(x, y) {
+ window.innerWidth = x;
+ window.innerHeight = y;
+
+ await waitForAnimationFrames();
+
+ await TestUtils.waitForCondition(
+ () => {
+ info(`window is ${window.innerWidth} x ${window.innerHeight}`);
+ if (
+ closeEnough(window.innerWidth, x) &&
+ closeEnough(window.innerHeight, y)
+ ) {
+ return true;
+ }
+ window.innerWidth = x;
+ window.innerHeight = y;
+ return false;
+ },
+ `Wait for ${x}x${y}`,
+ 250
+ );
+}
+
+async function waitForExpectedSize(helper, x, y) {
+ // Wait a few frames, this is generally enough for the resize to happen.
+ await waitForAnimationFrames();
+
+ let isExpectedSize = () => {
+ let box = helper.dialog._box;
+ info(`Dialog is ${box.clientWidth}x${box.clientHeight}`);
+ if (closeEnough(box.clientWidth, x) && closeEnough(box.clientHeight, y)) {
+ // Make sure there's an assertion so the test passes.
+ ok(true, `${box.clientWidth} close enough to ${x}`);
+ ok(true, `${box.clientHeight} close enough to ${y}`);
+ return true;
+ }
+ return false;
+ };
+
+ if (isExpectedSize()) {
+ // We can stop now if we hit the expected size.
+ return;
+ }
+
+ // In verify and debug runs sometimes this takes longer than expected,
+ // fallback to the slow method.
+ await TestUtils.waitForCondition(isExpectedSize, `Wait for ${x}x${y}`);
+}
+
+async function checkPreviewNavigationVisibility(expected) {
+ function isHidden(elem) {
+ // BTU.is_hidden can't handle shadow DOM elements
+ return !elem.getBoundingClientRect().height;
+ }
+
+ let previewStack = document.querySelector(".previewStack");
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ // move the mouse to a known position, then back to the preview to show the paginator
+ await mouseMoveAndWait(gURLBar.textbox);
+ await mouseMoveAndWait(previewStack);
+
+ ok(
+ BrowserTestUtils.is_visible(paginationElem),
+ "The preview pagination toolbar is visible"
+ );
+ for (let [id, visible] of Object.entries(expected)) {
+ let elem = paginationElem.shadowRoot.querySelector(`#${id}`);
+ if (visible) {
+ ok(!isHidden(elem), `Navigation element ${id} is visible`);
+ } else {
+ ok(isHidden(elem), `Navigation element ${id} is hidden`);
+ }
+ }
+}
+
+add_task(async function testResizing() {
+ await PrintHelper.withTestPage(async helper => {
+ let { innerWidth, innerHeight } = window;
+
+ await resizeWindow(500, 400);
+
+ await helper.startPrint();
+
+ let chromeHeight = window.windowUtils.getBoundsWithoutFlushing(
+ document.getElementById("browser")
+ ).top;
+
+ let initialWidth = 500 - 8;
+ let initialHeight = 400 - 16 - chromeHeight + 5;
+
+ await waitForExpectedSize(helper, initialWidth, initialHeight);
+
+ // check the preview pagination state for this window size
+ await checkPreviewNavigationVisibility({
+ navigateHome: false,
+ navigatePrevious: false,
+ navigateNext: false,
+ navigateEnd: false,
+ sheetIndicator: true,
+ });
+
+ await resizeWindow(600, 500);
+
+ await checkPreviewNavigationVisibility({
+ navigateHome: true,
+ navigatePrevious: true,
+ navigateNext: true,
+ navigateEnd: true,
+ sheetIndicator: true,
+ });
+
+ // 100 wider for window, add back the old 4px padding, it's now 16px * 2.
+ let updatedWidth = initialWidth + 100 + 8 - 32;
+ await waitForExpectedSize(helper, updatedWidth, initialHeight + 100);
+
+ await resizeWindow(1100, 900);
+
+ await waitForExpectedSize(helper, 1000, 650);
+
+ await checkPreviewNavigationVisibility({
+ navigateHome: true,
+ navigatePrevious: true,
+ navigateNext: true,
+ navigateEnd: true,
+ sheetIndicator: true,
+ });
+
+ await helper.closeDialog();
+
+ await resizeWindow(innerWidth, innerHeight);
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_page_change_print_original.js b/toolkit/components/printing/tests/browser_page_change_print_original.js
new file mode 100644
index 0000000000..4b85820466
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_page_change_print_original.js
@@ -0,0 +1,88 @@
+// This file spawns content tasks.
+/* global content */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+/**
+ * Verify that if the page contents change after print preview is initialized,
+ * and we re-initialize print preview (e.g. by changing page orientation),
+ * we still show (and will therefore print) the original contents.
+ */
+add_task(async function pp_after_orientation_change() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", false]],
+ });
+
+ const URI = TEST_PATH + "file_page_change_print_original_1.html";
+ // Can only do something if we have a print preview UI:
+ if (AppConstants.platform != "win" && AppConstants.platform != "linux") {
+ ok(true, "Can't test if there's no print preview.");
+ return;
+ }
+
+ // Ensure we get a browserStopped for this browser
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ URI,
+ false,
+ true
+ );
+ let browserToPrint = tab.linkedBrowser;
+ let ppBrowser = PrintPreviewListener.getPrintPreviewBrowser();
+
+ // Get a promise now that resolves when the original tab's location changes.
+ let originalTabNavigated = BrowserTestUtils.browserStopped(browserToPrint);
+
+ // Enter print preview:
+ let printPreviewEntered = PrintHelper.waitForOldPrintPreview(ppBrowser);
+ document.getElementById("cmd_printPreview").doCommand();
+ await printPreviewEntered;
+
+ // Assert that we are showing the original page
+ await SpecialPowers.spawn(ppBrowser, [], async function() {
+ is(
+ content.document.body.textContent.trim(),
+ "INITIAL PAGE",
+ "Should have initial page print previewed."
+ );
+ });
+
+ await originalTabNavigated;
+
+ // Change orientation and wait for print preview to re-enter:
+ let orient = PrintUtils.getPrintSettings().orientation;
+ let orientToSwitchTo =
+ orient != Ci.nsIPrintSettings.kPortraitOrientation
+ ? "portrait"
+ : "landscape";
+ let printPreviewToolbar = document.getElementById("print-preview-toolbar");
+
+ printPreviewEntered = PrintHelper.waitForOldPrintPreview(ppBrowser);
+ printPreviewToolbar.orient(orientToSwitchTo);
+ await printPreviewEntered;
+
+ // Check that we're still showing the original page.
+ await SpecialPowers.spawn(ppBrowser, [], async function() {
+ is(
+ content.document.body.textContent.trim(),
+ "INITIAL PAGE",
+ "Should still have initial page print previewed."
+ );
+ });
+
+ // Check that the other tab is definitely showing the new page:
+ await SpecialPowers.spawn(browserToPrint, [], async function() {
+ is(
+ content.document.body.textContent.trim(),
+ "REPLACED PAGE!",
+ "Original page should have changed."
+ );
+ });
+
+ PrintUtils.exitPrintPreview();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/printing/tests/browser_pdf_hidden_settings.js b/toolkit/components/printing/tests/browser_pdf_hidden_settings.js
new file mode 100644
index 0000000000..93c965fd94
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_pdf_hidden_settings.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const hiddenPdfIds = [
+ "margins",
+ "backgrounds",
+ "headers-footers",
+ "more-settings-options",
+];
+
+async function checkElements({ removed, file, testName }) {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ for (let id of hiddenPdfIds) {
+ is(
+ !helper.get(id),
+ removed,
+ `${id} is ${removed ? "" : "not "}removed (${testName})`
+ );
+ }
+
+ await helper.closeDialog();
+ }, file);
+}
+
+add_task(async function testSettingsShownForNonPdf() {
+ await checkElements({ removed: false, testName: "non-pdf" });
+});
+
+add_task(async function testSettingsHiddenForPdf() {
+ await checkElements({
+ removed: true,
+ file: "file_pdf.pdf",
+ testName: "pdf",
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_pdf_printer_settings.js b/toolkit/components/printing/tests/browser_pdf_printer_settings.js
new file mode 100644
index 0000000000..7ebabc9d01
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_pdf_printer_settings.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPDFPrinterSettings() {
+ await PrintHelper.withTestPage(async helper => {
+ // Set some bad prefs
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.print_to_file", false],
+ ["print.print_in_color", false],
+ ["print.printer_Mozilla_Save_to_PDF.print_to_file", false],
+ ["print.printer_Mozilla_Save_to_PDF.print_in_color", false],
+ ],
+ });
+
+ await helper.startPrint();
+ await helper.awaitAnimationFrame();
+
+ // Verify we end up with sane settings
+ let { settings } = helper;
+
+ ok(
+ settings.printToFile,
+ "Check the current settings have a truthy printToFile for the PDF printer"
+ );
+ ok(
+ settings.printInColor,
+ "Check the current settings have a truthy printInColor for the PDF printer"
+ );
+ is(
+ settings.outputFormat,
+ Ci.nsIPrintSettings.kOutputFormatPDF,
+ "The PDF printer has the correct outputFormat"
+ );
+
+ await helper.closeDialog();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function testPDFCancel() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ helper.mockFilePickerCancel();
+ let form = helper.doc.querySelector("#print");
+
+ // retrieve all elements other than cancel button
+ let elements = [];
+ for (let element of form.elements) {
+ if (element.name != "cancel") {
+ elements.push(element);
+ }
+ }
+ let getDisabledStates = () => elements.map(el => el.disabled);
+ let initialDisabledStates = getDisabledStates();
+
+ ok(
+ initialDisabledStates.some(disabled => !disabled),
+ "At least one enabled form element before submitting"
+ );
+ let getShownDisabledStates = new Promise(resolve => {
+ MockFilePicker.showCallback = () => resolve(getDisabledStates());
+ });
+
+ EventUtils.sendKey("return", helper.win);
+
+ let shownDisabledStates = await getShownDisabledStates;
+ ok(shownDisabledStates, "Got disabled states while shown");
+ ok(
+ shownDisabledStates.every(disabled => disabled),
+ "All elements were disabled when showing picker"
+ );
+ let cancelButton = helper.doc.querySelector(`button[name="cancel"]`);
+ ok(!cancelButton.disabled, "Cancel button is still enabled");
+
+ let saveButton = form.querySelector("#print-button");
+ await BrowserTestUtils.waitForAttributeRemoval("disabled", saveButton);
+ helper.assertDialogOpen();
+
+ is(
+ getDisabledStates().every(
+ (disabledState, index) => disabledState === initialDisabledStates[index]
+ ),
+ true,
+ "Previous disabled states match after returning to preview"
+ );
+
+ // Close the dialog with Escape.
+ await helper.withClosingFn(() => {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win);
+ });
+
+ helper.assertDialogClosed();
+ });
+});
+
+add_task(async function testPDFFile() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ helper.mockFilePicker("pdfFile.pdf");
+ let filePath = PathUtils.join(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "pdfFile.pdf"
+ );
+
+ await helper.withClosingFn(() => {
+ EventUtils.sendKey("return", helper.win);
+ });
+
+ is(await IOUtils.exists(filePath), true, "Saved pdf file exists");
+ ok(await IOUtils.read(filePath), "Saved pdf file is not empty");
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_preview_in_container.js b/toolkit/components/printing/tests/browser_preview_in_container.js
new file mode 100644
index 0000000000..21a0d1e6b1
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_preview_in_container.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+async function runTest() {
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ gBrowser.selectedTab = await BrowserTestUtils.addTab(
+ gBrowser,
+ `${TEST_PATH}file_print.html`,
+ { userContextId: 1 }
+ );
+
+ // Wait for window.print() to run and ensure we're showing the preview...
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ let previewBrowser = document.querySelector(".printPreviewBrowser");
+ let contentFound = await SpecialPowers.spawn(previewBrowser, [], () => {
+ return !!content.document.getElementById("printed");
+ });
+ ok(contentFound, "We should find the preview content.");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(async function test_in_container() {
+ // window.print() only shows print preview when print.tab_modal.enabled is
+ // true.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.tab_modal.enabled", true],
+ ["privacy.firstparty.isolate", false],
+ ],
+ });
+
+ await runTest();
+});
+
+add_task(async function test_with_fpi() {
+ // window.print() only shows print preview when print.tab_modal.enabled is
+ // true.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.tab_modal.enabled", true],
+ ["privacy.firstparty.isolate", true],
+ ],
+ });
+ await runTest();
+});
diff --git a/toolkit/components/printing/tests/browser_preview_navigation.js b/toolkit/components/printing/tests/browser_preview_navigation.js
new file mode 100644
index 0000000000..d90d95416c
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_preview_navigation.js
@@ -0,0 +1,424 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function compare10nArgs(elem, expectedValues) {
+ let l10nArgs = elem.ownerDocument.l10n.getAttributes(elem).args;
+ for (let [name, value] of Object.entries(expectedValues)) {
+ if (value !== l10nArgs[name]) {
+ info(
+ `compare10nArgs, expected ${name}: ${value}, actual: ${l10nArgs[name]}`
+ );
+ return false;
+ }
+ }
+ return true;
+}
+
+async function waitForPageStatusUpdate(elem, expected, message) {
+ await TestUtils.waitForCondition(
+ () => compare10nArgs(elem, expected),
+ message
+ );
+}
+
+async function waitUntilVisible(elem, visible = true) {
+ await TestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_visible(elem) &&
+ getComputedStyle(elem).opacity == "1",
+ "Waiting for element to be visible and have opacity:1"
+ );
+}
+
+async function waitUntilTransparent(elem) {
+ // Note that is_visible considers a fully transparent element "visible"
+ await TestUtils.waitForCondition(
+ () => getComputedStyle(elem).opacity == "0",
+ "Waiting for element to be have opacity:0"
+ );
+}
+
+async function mouseMoveAndWait(elem) {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(elem, "mousemove");
+ EventUtils.synthesizeMouseAtCenter(elem, { type: "mousemove" });
+ await mouseMovePromise;
+ await TestUtils.waitForTick();
+}
+
+add_task(async function testToolbarVisibility() {
+ // move the mouse to a known position
+ await mouseMoveAndWait(gURLBar.textbox);
+
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ let previewStack = document.querySelector(".previewStack");
+
+ // The toolbar has 0 opacity until we hover or focus it
+ is(getComputedStyle(paginationElem).opacity, "0", "Initially transparent");
+
+ let visiblePromise = waitUntilVisible(paginationElem);
+ paginationElem.shadowRoot.querySelector("#navigateEnd").focus();
+ await visiblePromise;
+ is(
+ getComputedStyle(paginationElem).opacity,
+ "1",
+ "Opaque with button focused"
+ );
+
+ await EventUtils.synthesizeKey("KEY_Tab", {});
+ await waitUntilTransparent(paginationElem);
+ is(getComputedStyle(paginationElem).opacity, "0", "Returns to transparent");
+
+ visiblePromise = waitUntilVisible(paginationElem);
+ info("Waiting for mousemove event, and for the toolbar to become opaque");
+ await mouseMoveAndWait(previewStack);
+ await visiblePromise;
+ is(getComputedStyle(paginationElem).opacity, "1", "Opaque toolbar");
+
+ // put the mouse back where it won't interfere with later tests
+ await mouseMoveAndWait(gURLBar.textbox);
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testPreviewSheetCount() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ let paginationSheetIndicator = paginationElem.shadowRoot.querySelector(
+ "#sheetIndicator"
+ );
+
+ // We have to wait for the first _updatePrintPreview to get the sheet count
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Paginator indicates the correct number of sheets"
+ );
+
+ // then switch to page range 1-1 and verify page count changes
+ await helper.dispatchSettingsChange({
+ pageRanges: ["1", "1"],
+ });
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 1 },
+ "Indicates the updated number of sheets"
+ );
+
+ await helper.closeDialog();
+ }, "longerArticle.html");
+});
+
+add_task(async function testPreviewScroll() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ let paginationSheetIndicator = paginationElem.shadowRoot.querySelector(
+ "#sheetIndicator"
+ );
+ // Wait for the first _updatePrintPreview before interacting with the preview
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Paginator indicates the correct number of sheets"
+ );
+ let previewBrowser = PrintUtils.getPreviewBrowser();
+
+ // scroll down the document
+ // and verify the indicator is updated correctly
+ await SpecialPowers.spawn(previewBrowser, [], async function() {
+ const { ContentTaskUtils } = ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ content.focus();
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", {}, content);
+ });
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 2, sheetCount: 3 },
+ "Indicator updates on scroll"
+ );
+
+ // move focus before closing the dialog
+ helper.get("cancel-button").focus();
+ await helper.closeDialog();
+ }, "longerArticle.html");
+});
+
+add_task(async function testPreviewNavigationCommands() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ let paginationSheetIndicator = paginationElem.shadowRoot.querySelector(
+ "#sheetIndicator"
+ );
+ // Wait for the first _updatePrintPreview before interacting with the preview
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Paginator indicates the correct number of sheets"
+ );
+
+ // click the navigation buttons
+ // and verify the indicator is updated correctly
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 2, sheetCount: 3 },
+ "Indicator updates on navigation"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigatePrevious"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Indicator updates on navigation to previous"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateEnd"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 3, sheetCount: 3 },
+ "Indicator updates on navigation to end"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateHome"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Indicator updates on navigation to start"
+ );
+
+ // Test rapid clicks on the navigation buttons
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 3, sheetCount: 3 },
+ "2 successive 'next' clicks correctly update the sheet indicator"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigatePrevious"),
+ {}
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigatePrevious"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "2 successive 'previous' clicks correctly update the sheet indicator"
+ );
+
+ // move focus before closing the dialog
+ helper.get("cancel-button").focus();
+ await helper.closeDialog();
+ }, "longerArticle.html");
+});
+
+add_task(async function testMultiplePreviewNavigation() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ const tab1 = gBrowser.selectedTab;
+ let sheetIndicator = document
+ .querySelector(".printPreviewNavigation")
+ .shadowRoot.querySelector("#sheetIndicator");
+
+ await waitForPageStatusUpdate(
+ sheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Indicator has the correct initial sheetCount"
+ );
+
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ PrintHelper.defaultTestPageUrl
+ );
+ let helper2 = new PrintHelper(tab2.linkedBrowser);
+ await helper2.startPrint();
+
+ let [previewBrowser1, previewBrowser2] = document.querySelectorAll(
+ ".printPreviewBrowser[previewtype='primary']"
+ );
+ ok(previewBrowser1 && previewBrowser2, "There are 2 preview browsers");
+
+ let [toolbar1, toolbar2] = document.querySelectorAll(
+ ".printPreviewNavigation"
+ );
+ ok(toolbar1 && toolbar2, "There are 2 preview navigation toolbars");
+ is(
+ toolbar1.previewBrowser,
+ previewBrowser1,
+ "toolbar1 has the correct previewBrowser"
+ );
+ sheetIndicator = toolbar1.shadowRoot.querySelector("#sheetIndicator");
+ ok(
+ compare10nArgs(sheetIndicator, { sheetNum: 1, sheetCount: 3 }),
+ "First toolbar has the correct content"
+ );
+
+ is(
+ toolbar2.previewBrowser,
+ previewBrowser2,
+ "toolbar2 has the correct previewBrowser"
+ );
+ sheetIndicator = toolbar2.shadowRoot.querySelector("#sheetIndicator");
+ ok(
+ compare10nArgs(sheetIndicator, { sheetNum: 1, sheetCount: 1 }),
+ "2nd toolbar has the correct content"
+ );
+
+ // Switch back to the first tab and ensure the correct preview navigation is updated when clicked
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ sheetIndicator = toolbar1.shadowRoot.querySelector("#sheetIndicator");
+
+ EventUtils.synthesizeMouseAtCenter(
+ toolbar1.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ sheetIndicator,
+ { sheetNum: 2, sheetCount: 3 },
+ "Indicator updates on navigation"
+ );
+
+ gBrowser.removeTab(tab2);
+ }, "longerArticle.html");
+});
+
+add_task(async function testPreviewNavigationSelection() {
+ await PrintHelper.withTestPage(async helper => {
+ await SpecialPowers.spawn(helper.sourceBrowser, [], async function() {
+ let element = content.document.querySelector("#page-2");
+ content.window.getSelection().selectAllChildren(element);
+ });
+
+ await helper.startPrint();
+
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ let paginationSheetIndicator = paginationElem.shadowRoot.querySelector(
+ "#sheetIndicator"
+ );
+ // Wait for the first _updatePrintPreview before interacting with the preview
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Paginator indicates the correct number of sheets"
+ );
+
+ // click a navigation button
+ // and verify the indicator is updated correctly
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 2, sheetCount: 3 },
+ "Indicator updates on navigation"
+ );
+
+ await helper.openMoreSettings();
+ let printSelect = helper.get("print-selection-container");
+ await helper.waitForPreview(() => helper.click(printSelect));
+
+ // Wait for the first _updatePrintPreview before interacting with the preview
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 2 },
+ "Paginator indicates the correct number of sheets"
+ );
+
+ // click a navigation button
+ // and verify the indicator is updated correctly
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 2, sheetCount: 2 },
+ "Indicator updates on navigation"
+ );
+
+ // move focus before closing the dialog
+ helper.get("cancel-button").focus();
+ await helper.closeDialog();
+ }, "longerArticle.html");
+});
+
+add_task(async function testPaginatorAfterSettingsUpdate() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+
+ let paginationElem = document.querySelector(".printPreviewNavigation");
+ let paginationSheetIndicator = paginationElem.shadowRoot.querySelector(
+ "#sheetIndicator"
+ );
+ // Wait for the first _updatePrintPreview before interacting with the preview
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Paginator indicates the correct number of sheets"
+ );
+
+ // click the navigation buttons
+ // and verify the indicator is updated correctly
+ EventUtils.synthesizeMouseAtCenter(
+ paginationElem.shadowRoot.querySelector("#navigateNext"),
+ {}
+ );
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 2, sheetCount: 3 },
+ "Indicator updates on navigation"
+ );
+
+ // Select a new printer
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await waitForPageStatusUpdate(
+ paginationSheetIndicator,
+ { sheetNum: 1, sheetCount: 3 },
+ "Indicator updates on navigation"
+ );
+ ok(
+ compare10nArgs(paginationSheetIndicator, { sheetNum: 1, sheetCount: 3 }),
+ "Sheet indicator has correct value"
+ );
+
+ // move focus before closing the dialog
+ helper.get("cancel-button").focus();
+ await helper.closeDialog();
+ }, "longerArticle.html");
+});
diff --git a/toolkit/components/printing/tests/browser_preview_print_coop.js b/toolkit/components/printing/tests/browser_preview_print_coop.js
new file mode 100644
index 0000000000..b635ca82a7
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_preview_print_coop.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+/**
+ * Verify if the page with a COOP header can be used for printing preview.
+ */
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", false]],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `${TEST_PATH}file_coop_header.html`
+ );
+
+ // Enter print preview
+ let ppBrowser = PrintPreviewListener.getPrintPreviewBrowser();
+ let ppPromise = PrintHelper.waitForOldPrintPreview(ppBrowser);
+ document.getElementById("cmd_printPreview").doCommand();
+ await ppPromise;
+
+ await BrowserTestUtils.waitForCondition(
+ () => gInPrintPreviewMode,
+ "Should be in print preview mode now."
+ );
+
+ ok(true, "We did not crash.");
+
+ PrintUtils.exitPrintPreview();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js b/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
new file mode 100644
index 0000000000..681e53f8b9
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
@@ -0,0 +1,102 @@
+/**
+ * Verify if we recover from parsing errors of Reader Mode when
+ * Simplify Page checkbox is checked.
+ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+add_task(async function set_simplify_and_reader_pref() {
+ // Ensure we have the simplify page preference set
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.tab_modal.enabled", false],
+ ["print.use_simplify_page", true],
+ ["reader.parse-on-load.enabled", true],
+ ],
+ });
+});
+
+add_task(async function switch_print_preview_browsers() {
+ let url = TEST_PATH + "simplifyNonArticleSample.html";
+
+ // Can only do something if we have a print preview UI:
+ if (AppConstants.platform != "win" && AppConstants.platform != "linux") {
+ ok(false, "Can't test if there's no print preview.");
+ return;
+ }
+
+ // Ensure we get a browserStopped for this browser
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ false,
+ true
+ );
+
+ // Trick browser to think loaded tab has isArticle property set as true
+ tab.linkedBrowser.isArticle = true;
+
+ // Enter print preview
+ let defaultPPBrowser = PrintPreviewListener.getPrintPreviewBrowser();
+ let defaultPPEntered = PrintHelper.waitForOldPrintPreview(defaultPPBrowser);
+ document.getElementById("cmd_printPreview").doCommand();
+ await defaultPPEntered;
+
+ // Assert that we are showing the initial content on default print preview browser
+ await SpecialPowers.spawn(defaultPPBrowser, [], async function() {
+ is(
+ content.document.title,
+ "Non article title",
+ "Should have initial content."
+ );
+ });
+
+ // Here we call simplified mode
+ let simplifiedPPBrowser = PrintPreviewListener.getSimplifiedPrintPreviewBrowser();
+ let simplifiedPPEntered = PrintHelper.waitForOldPrintPreview(
+ simplifiedPPBrowser
+ );
+ let printPreviewToolbar = document.getElementById("print-preview-toolbar");
+
+ // Wait for simplify page option enablement
+ await BrowserTestUtils.waitForCondition(() => {
+ return !printPreviewToolbar.mSimplifyPageCheckbox.disabled;
+ });
+
+ printPreviewToolbar.mSimplifyPageCheckbox.click();
+ await simplifiedPPEntered;
+
+ // Assert that simplify page option is checked
+ is(
+ printPreviewToolbar.mSimplifyPageCheckbox.checked,
+ true,
+ "Should have simplify page option checked"
+ );
+
+ // Assert that we are showing recovery content on simplified print preview browser
+ await SpecialPowers.spawn(simplifiedPPBrowser, [], async function() {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.title === "Failed to load article from page",
+ "Simplified document title should be updated with recovery title."
+ );
+ });
+
+ // Assert that we are selecting simplified print preview browser, and not default one
+ is(
+ gBrowser.selectedTab.linkedBrowser,
+ simplifiedPPBrowser,
+ "Should have simplified print preview browser selected"
+ );
+ isnot(
+ gBrowser.selectedTab.linkedBrowser,
+ defaultPPBrowser,
+ "Should not have default print preview browser selected"
+ );
+
+ PrintUtils.exitPrintPreview();
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/printing/tests/browser_preview_switch_print_selected.js b/toolkit/components/printing/tests/browser_preview_switch_print_selected.js
new file mode 100644
index 0000000000..e2771a9e3c
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_preview_switch_print_selected.js
@@ -0,0 +1,126 @@
+/**
+ * Verify if we correctly switch print preview browsers based on whether
+ * Simplify Page checkbox is checked.
+ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+add_task(async function set_simplify_and_reader_pref() {
+ // Ensure we have the simplify page preference set
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.tab_modal.enabled", false],
+ ["print.use_simplify_page", true],
+ ["reader.parse-on-load.enabled", true],
+ ],
+ });
+});
+
+add_task(async function switch_print_preview_browsers() {
+ let url = TEST_PATH + "simplifyArticleSample.html";
+
+ // Can only do something if we have a print preview UI:
+ if (AppConstants.platform != "win" && AppConstants.platform != "linux") {
+ ok(false, "Can't test if there's no print preview.");
+ return;
+ }
+
+ // Ensure we get a browserStopped for this browser
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ false,
+ true
+ );
+
+ // Wait for Reader Mode to parse and set property of loaded tab
+ await BrowserTestUtils.waitForCondition(() => {
+ return tab.linkedBrowser.isArticle;
+ });
+
+ // Enter print preview
+ let defaultPPBrowser = PrintPreviewListener.getPrintPreviewBrowser();
+ let defaultPPEntered = PrintHelper.waitForOldPrintPreview(defaultPPBrowser);
+ document.getElementById("cmd_printPreview").doCommand();
+ await defaultPPEntered;
+
+ // Assert that we are showing the initial content on default print preview browser
+ await SpecialPowers.spawn(defaultPPBrowser, [], async function() {
+ is(content.document.title, "Article title", "Should have initial content.");
+ });
+
+ // Here we call simplified mode
+ let simplifiedPPBrowser = PrintPreviewListener.getSimplifiedPrintPreviewBrowser();
+ let simplifiedPPEntered = PrintHelper.waitForOldPrintPreview(
+ simplifiedPPBrowser
+ );
+ let printPreviewToolbar = document.getElementById("print-preview-toolbar");
+
+ // Wait for simplify page option enablement
+ await BrowserTestUtils.waitForCondition(() => {
+ return !printPreviewToolbar.mSimplifyPageCheckbox.disabled;
+ });
+
+ printPreviewToolbar.mSimplifyPageCheckbox.click();
+ await simplifiedPPEntered;
+
+ // Assert that simplify page option is checked
+ is(
+ printPreviewToolbar.mSimplifyPageCheckbox.checked,
+ true,
+ "Should have simplify page option checked"
+ );
+
+ // Assert that we are showing custom content on simplified print preview browser
+ await SpecialPowers.spawn(simplifiedPPBrowser, [], async function() {
+ is(content.document.title, "Article title", "Should have custom content.");
+ });
+
+ // Assert that we are selecting simplified print preview browser, and not default one
+ is(
+ gBrowser.selectedTab.linkedBrowser,
+ simplifiedPPBrowser,
+ "Should have simplified print preview browser selected"
+ );
+ isnot(
+ gBrowser.selectedTab.linkedBrowser,
+ defaultPPBrowser,
+ "Should not have default print preview browser selected"
+ );
+
+ // Switch back to default print preview content
+ defaultPPEntered = PrintHelper.waitForOldPrintPreview(defaultPPBrowser);
+ printPreviewToolbar.mSimplifyPageCheckbox.click();
+ await defaultPPEntered;
+
+ // Assert that simplify page option is not checked
+ isnot(
+ printPreviewToolbar.mSimplifyPageCheckbox.checked,
+ true,
+ "Should not have simplify page option checked"
+ );
+
+ // Assert that we are showing the initial content on default print preview browser
+ await SpecialPowers.spawn(defaultPPBrowser, [], async function() {
+ is(content.document.title, "Article title", "Should have initial content.");
+ });
+
+ // Assert that we are selecting default print preview browser, and not simplified one
+ is(
+ gBrowser.selectedTab.linkedBrowser,
+ defaultPPBrowser,
+ "Should have default print preview browser selected"
+ );
+ isnot(
+ gBrowser.selectedTab.linkedBrowser,
+ simplifiedPPBrowser,
+ "Should not have simplified print preview browser selected"
+ );
+
+ PrintUtils.exitPrintPreview();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/printing/tests/browser_print_bcg_id_overflow.js b/toolkit/components/printing/tests/browser_print_bcg_id_overflow.js
new file mode 100644
index 0000000000..a86db21c8b
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_bcg_id_overflow.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+// The actual uri we open doesn't really matter.
+const OPENED_URI = PrintHelper.defaultTestPageUrl;
+
+// Test for bug 1669554:
+//
+// This opens a rel=noopener window in a content process, which causes us to
+// create a browsing context with an id that likely overflows an int32_t, which
+// caused us to fail to parse the initialBrowsingContextGroupId attribute
+// (causing us to potentially clone in the wrong process, etc).
+const OPEN_NOOPENER_WINDOW = `
+ <a rel="noopener" target="_blank" href="${OPENED_URI}">Open the window</a>
+`;
+
+add_task(async function test_bc_id_overflow() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ is(document.querySelector(".printPreviewBrowser"), null);
+
+ await BrowserTestUtils.withNewTab(
+ `data:text/html,` + encodeURIComponent(OPEN_NOOPENER_WINDOW),
+ async function(browser) {
+ let tabOpenedPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ OPENED_URI,
+ /* waitForLoad = */ true
+ );
+ await BrowserTestUtils.synthesizeMouse("a", 0, 0, {}, browser);
+ let tab = await tabOpenedPromise;
+ let helper = new PrintHelper(tab.linkedBrowser);
+ await helper.startPrint();
+ helper.assertDialogOpen();
+
+ let previewBrowser = document.querySelector(".printPreviewBrowser");
+ is(typeof previewBrowser.browsingContext.group.id, "number", "Sanity");
+ is(
+ previewBrowser.browsingContext.group.id,
+ tab.linkedBrowser.browsingContext.group.id,
+ "Group ids should match: " + tab.linkedBrowser.browsingContext.group.id
+ );
+ is(
+ previewBrowser.browsingContext.group,
+ tab.linkedBrowser.browsingContext.group,
+ "Groups should match"
+ );
+ await helper.closeDialog();
+ await BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
diff --git a/toolkit/components/printing/tests/browser_print_context_menu.js b/toolkit/components/printing/tests/browser_print_context_menu.js
new file mode 100644
index 0000000000..023d6eb794
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_context_menu.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const frameSource = `<a href="about:mozilla">Inner frame</a>`;
+const source = `<html><h1>Top level text</h1><iframe srcdoc='${frameSource}' id="f"></iframe></html>`;
+
+add_task(async function testPrintFrame() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ let url = `data:text/html,${source}`;
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async function(browser) {
+ let contentAreaContextMenuPopup = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#f",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ let frameContextMenu = document.getElementById("frame");
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ frameContextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(frameContextMenu, {});
+ await popupShownPromise;
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ frameContextMenu,
+ "popuphidden"
+ );
+ let item = document.getElementById("context-printframe");
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ await popupHiddenPromise;
+
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ let previewBrowser = document.querySelector(
+ ".printPreviewBrowser[previewtype='primary']"
+ );
+ let helper = new PrintHelper(browser);
+
+ let textContent = await TestUtils.waitForCondition(() =>
+ SpecialPowers.spawn(previewBrowser, [], function() {
+ return content.document.body.textContent;
+ })
+ );
+
+ is(textContent, "Inner frame", "Correct content loaded");
+ is(
+ helper.win.PrintEventHandler.printFrameOnly,
+ true,
+ "Print frame only is true"
+ );
+ PrintHelper.resetPrintPrefs();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_copies.js b/toolkit/components/printing/tests/browser_print_copies.js
new file mode 100644
index 0000000000..083dd6e5ee
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_copies.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testCopyError() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter("A printer");
+ await SpecialPowers.pushPrefEnv({
+ set: [["print_printer", "A printer"]],
+ });
+
+ await helper.startPrint();
+
+ let copyInput = helper.get("copies-count");
+ let destinationPicker = helper.get("printer-picker");
+
+ await helper.assertSettingsChanged(
+ { numCopies: 1 },
+ { numCopies: 10000 },
+ async () => {
+ helper.text(copyInput, "10000000000");
+
+ // Initially, the copies will be more than the max.
+ is(copyInput.checkValidity(), false, "Copy count is invalid");
+ is(
+ destinationPicker.disabled,
+ false,
+ "Destination picker is still enabled"
+ );
+
+ // When the settings event happens, copies resolves to max.
+ await helper.waitForSettingsEvent();
+ is(copyInput.value, "10000", "Copies gets set to max value");
+ is(copyInput.checkValidity(), true, "Copy count is valid again");
+ }
+ );
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_duplex.js b/toolkit/components/printing/tests/browser_print_duplex.js
new file mode 100644
index 0000000000..5fa59f4672
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_duplex.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPDFPrinterIsNonDuplex() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+
+ is(
+ helper.settings.printerName,
+ "Mozilla Save to PDF",
+ "Mozilla Save to PDF is the current printer."
+ );
+
+ const duplexSection = helper.get("two-sided-printing");
+ ok(
+ duplexSection.hidden,
+ "The two-sided printing section should be hidden when the printer does not support duplex."
+ );
+
+ helper.assertSettingsMatch({ duplex: Ci.nsIPrintSettings.kSimplex });
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testToggleDuplexWithPortraitOrientation() {
+ const mockPrinterName = "DuplexWithPortrait";
+ await PrintHelper.withTestPage(async helper => {
+ const printer = helper.addMockPrinter(mockPrinterName);
+ printer.supportsDuplex = Promise.resolve(true);
+
+ await helper.startPrint();
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await helper.awaitAnimationFrame();
+ await helper.openMoreSettings();
+
+ is(
+ helper.settings.printerName,
+ mockPrinterName,
+ "The Fake Printer is current printer"
+ );
+
+ const duplexSection = helper.get("two-sided-printing");
+ ok(
+ !duplexSection.hidden,
+ "The two-sided printing section should not be hidden when the printer supports duplex."
+ );
+
+ helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kSimplex,
+ });
+
+ await helper.click(helper.get("duplex-enabled"));
+ helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kDuplexHorizontal,
+ });
+
+ await helper.click(helper.get("duplex-enabled"));
+ helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kSimplex,
+ });
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testToggleDuplexWithLandscapeOrientation() {
+ const mockPrinterName = "DuplexWithLandscape";
+ await PrintHelper.withTestPage(async helper => {
+ const printer = helper.addMockPrinter(mockPrinterName);
+ printer.supportsDuplex = Promise.resolve(true);
+
+ await helper.startPrint();
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await helper.awaitAnimationFrame();
+ await helper.openMoreSettings();
+
+ is(
+ helper.settings.printerName,
+ mockPrinterName,
+ "The Fake Printer is current printer"
+ );
+
+ const duplexSection = helper.get("two-sided-printing");
+ ok(
+ !duplexSection.hidden,
+ "The two-sided printing section should not be hidden when the printer supports duplex."
+ );
+
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kSimplex,
+ });
+
+ await helper.dispatchSettingsChange({ orientation: 1 });
+ await helper.awaitAnimationFrame();
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kLandscapeOrientation,
+ duplex: Ci.nsIPrintSettings.kSimplex,
+ });
+
+ await helper.click(helper.get("duplex-enabled"));
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kLandscapeOrientation,
+ duplex: Ci.nsIPrintSettings.kDuplexHorizontal,
+ });
+
+ await helper.click(helper.get("duplex-enabled"));
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kLandscapeOrientation,
+ duplex: Ci.nsIPrintSettings.kSimplex,
+ });
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testSwitchOrientationWithDuplexEnabled() {
+ const mockPrinterName = "ToggleOrientationPrinter";
+ await PrintHelper.withTestPage(async helper => {
+ const printer = helper.addMockPrinter(mockPrinterName);
+ printer.supportsDuplex = Promise.resolve(true);
+
+ await helper.startPrint();
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await helper.awaitAnimationFrame();
+ await helper.openMoreSettings();
+
+ is(
+ helper.settings.printerName,
+ mockPrinterName,
+ "The Fake Printer is current printer"
+ );
+
+ const duplexSection = helper.get("two-sided-printing");
+ ok(
+ !duplexSection.hidden,
+ "The two-sided printing section should not be hidden when the printer supports duplex."
+ );
+
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kSimplex,
+ });
+
+ await helper.click(helper.get("duplex-enabled"));
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kDuplexHorizontal,
+ });
+
+ await helper.dispatchSettingsChange({ orientation: 1 });
+ await helper.awaitAnimationFrame();
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kLandscapeOrientation,
+ duplex: Ci.nsIPrintSettings.kDuplexHorizontal,
+ });
+
+ await helper.dispatchSettingsChange({ orientation: 0 });
+ await helper.awaitAnimationFrame();
+ await helper.assertSettingsMatch({
+ orientation: Ci.nsIPrintSettings.kPortraitOrientation,
+ duplex: Ci.nsIPrintSettings.kDuplexHorizontal,
+ });
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_in_container.js b/toolkit/components/printing/tests/browser_print_in_container.js
new file mode 100644
index 0000000000..243b07eaa8
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_in_container.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ let tab = await BrowserTestUtils.switchTab(gBrowser, function() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ `${TEST_PATH}simplifyArticleSample.html`,
+ { userContextId: 1 }
+ );
+ });
+
+ const helper = new PrintHelper(tab.linkedBrowser);
+
+ helper.assertDialogClosed();
+ await helper.startPrint();
+ helper.assertDialogOpen();
+
+ let file = helper.mockFilePicker("browser_print_in_container.pdf");
+ await helper.assertPrintToFile(file, () => {
+ helper.click(helper.get("print-button"));
+ });
+
+ ok(true, "We did not crash.");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/printing/tests/browser_print_margins.js b/toolkit/components/printing/tests/browser_print_margins.js
new file mode 100644
index 0000000000..23ab1c81f7
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_margins.js
@@ -0,0 +1,802 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function changeDefaultToCustom(helper) {
+ let marginSelect = helper.get("margins-picker");
+ marginSelect.focus();
+ marginSelect.scrollIntoView({ block: "center" });
+ EventUtils.sendKey("space", helper.win);
+ EventUtils.sendKey("down", helper.win);
+ EventUtils.sendKey("down", helper.win);
+ EventUtils.sendKey("down", helper.win);
+ EventUtils.sendKey("return", helper.win);
+}
+
+function changeCustomToDefault(helper) {
+ let marginSelect = helper.get("margins-picker");
+ marginSelect.focus();
+ EventUtils.sendKey("space", helper.win);
+ EventUtils.sendKey("up", helper.win);
+ EventUtils.sendKey("up", helper.win);
+ EventUtils.sendKey("up", helper.win);
+ EventUtils.sendKey("return", helper.win);
+}
+
+function changeCustomToNone(helper) {
+ let marginSelect = helper.get("margins-picker");
+ marginSelect.focus();
+ EventUtils.sendKey("space", helper.win);
+ EventUtils.sendKey("up", helper.win);
+ EventUtils.sendKey("return", helper.win);
+}
+
+function assertPendingMarginsUpdate(helper) {
+ ok(
+ Object.keys(helper.win.PrintEventHandler._delayedChanges).length,
+ "At least one delayed task is added"
+ );
+ ok(
+ helper.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
+ "The update task is armed"
+ );
+}
+
+function assertNoPendingMarginsUpdate(helper) {
+ ok(
+ !helper.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
+ "The update task isn't armed"
+ );
+}
+
+async function setupLetterPaper() {
+ const INCHES_PER_POINT = 1 / 72;
+ const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance(
+ Ci.nsIPrinterList
+ );
+ let fallbackPaperList = await printerList.fallbackPaperList;
+ let paper = fallbackPaperList.find(
+ paper =>
+ paper.width * INCHES_PER_POINT == 8.5 &&
+ paper.height * INCHES_PER_POINT == 11
+ );
+ ok(paper, "Found a paper");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_id", paper.id.toString()],
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_size_unit", 0],
+ [
+ "print.printer_Mozilla_Save_to_PDF.print_paper_width",
+ (paper.width * INCHES_PER_POINT).toString(),
+ ],
+ [
+ "print.printer_Mozilla_Save_to_PDF.print_paper_height",
+ (paper.height * INCHES_PER_POINT).toString(),
+ ],
+ ],
+ });
+}
+
+add_task(async function testPresetMargins() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 0.25, marginRight: 1, marginBottom: 2, marginLeft: 0.75 },
+ async () => {
+ let marginSelect = helper.get("margins-picker");
+ let customMargins = helper.get("custom-margins");
+
+ ok(customMargins.hidden, "Custom margins are hidden");
+ is(marginSelect.value, "default", "Default margins set");
+
+ this.changeDefaultToCustom(helper);
+
+ is(marginSelect.value, "custom", "Custom margins are now set");
+ ok(!customMargins.hidden, "Custom margins are present");
+ // Check that values are initialized to correct values
+ is(
+ helper.get("custom-margin-top").value,
+ "0.50",
+ "Top margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-right").value,
+ "0.50",
+ "Right margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-bottom").value,
+ "0.50",
+ "Bottom margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-left").value,
+ "0.50",
+ "Left margin placeholder is correct"
+ );
+
+ await helper.awaitAnimationFrame();
+
+ await helper.text(helper.get("custom-margin-top"), "0.25");
+ await helper.text(helper.get("custom-margin-right"), "1");
+ await helper.text(helper.get("custom-margin-bottom"), "2");
+ await helper.text(helper.get("custom-margin-left"), "0.75");
+
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ }
+ );
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testHeightError() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+
+ await helper.assertSettingsNotChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ let marginError = helper.get("error-invalid-margin");
+ ok(marginError.hidden, "Margin error is hidden");
+
+ await helper.text(helper.get("custom-margin-top"), "20");
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError);
+
+ ok(!marginError.hidden, "Margin error is showing");
+ assertNoPendingMarginsUpdate(helper);
+ }
+ );
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testWidthError() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+
+ await helper.assertSettingsNotChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ let marginError = helper.get("error-invalid-margin");
+ ok(marginError.hidden, "Margin error is hidden");
+
+ await helper.text(helper.get("custom-margin-right"), "20");
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError);
+
+ ok(!marginError.hidden, "Margin error is showing");
+ assertNoPendingMarginsUpdate(helper);
+ }
+ );
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testInvalidMarginsReset() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.assertSettingsNotChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ ok(marginError.hidden, "Margin error is hidden");
+
+ await helper.awaitAnimationFrame();
+ await helper.text(helper.get("custom-margin-top"), "20");
+ await helper.text(helper.get("custom-margin-right"), "20");
+ assertNoPendingMarginsUpdate(helper);
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError);
+ }
+ );
+
+ this.changeCustomToDefault(helper);
+ assertNoPendingMarginsUpdate(helper);
+ await BrowserTestUtils.waitForCondition(
+ () => marginError.hidden,
+ "Wait for margin error to be hidden"
+ );
+ this.changeDefaultToCustom(helper);
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ });
+
+ is(
+ helper.get("margins-picker").value,
+ "custom",
+ "The custom option is selected"
+ );
+ is(
+ helper.get("custom-margin-top").value,
+ "0.50",
+ "Top margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-right").value,
+ "0.50",
+ "Right margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-bottom").value,
+ "0.50",
+ "Bottom margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-left").value,
+ "0.50",
+ "Left margin placeholder is correct"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => marginError.hidden,
+ "Wait for margin error to be hidden"
+ );
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testChangeInvalidToValidUpdate() {
+ await PrintHelper.withTestPage(async helper => {
+ await setupLetterPaper();
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.text(helper.get("custom-margin-bottom"), "11");
+ assertNoPendingMarginsUpdate(helper);
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError);
+ ok(!marginError.hidden, "Margin error is showing");
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ paperId: "na_letter",
+ });
+
+ await helper.text(helper.get("custom-margin-top"), "1");
+ assertNoPendingMarginsUpdate(helper);
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ });
+ ok(!marginError.hidden, "Margin error is showing");
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 1, marginRight: 0.5, marginBottom: 1, marginLeft: 0.5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-bottom"), "1");
+
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+ });
+});
+
+add_task(async function testChangeInvalidCanRevalidate() {
+ await PrintHelper.withTestPage(async helper => {
+ await setupLetterPaper();
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 5, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-top"), "5");
+ await helper.text(helper.get("custom-margin-bottom"), "3");
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+
+ await helper.text(helper.get("custom-margin-top"), "9");
+ assertNoPendingMarginsUpdate(helper);
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError);
+ ok(!marginError.hidden, "Margin error is showing");
+ helper.assertSettingsMatch({
+ marginTop: 5,
+ marginRight: 0.5,
+ marginBottom: 3,
+ marginLeft: 0.5,
+ paperId: "na_letter",
+ });
+
+ await helper.assertSettingsChanged(
+ { marginTop: 5, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 },
+ { marginTop: 9, marginRight: 0.5, marginBottom: 2, marginLeft: 0.5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-bottom"), "2");
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+ });
+});
+
+add_task(async function testCustomMarginsPersist() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 0.25, marginRight: 1, marginBottom: 2, marginLeft: 0 },
+ async () => {
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+
+ await helper.text(helper.get("custom-margin-top"), "0.25");
+ await helper.text(helper.get("custom-margin-right"), "1");
+ await helper.text(helper.get("custom-margin-bottom"), "2");
+ await helper.text(helper.get("custom-margin-left"), "0");
+
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ }
+ );
+
+ await helper.closeDialog();
+
+ await helper.startPrint();
+ await helper.openMoreSettings();
+
+ helper.assertSettingsMatch({
+ marginTop: 0.25,
+ marginRight: 1,
+ marginBottom: 2,
+ marginLeft: 0,
+ });
+
+ is(
+ helper.get("margins-picker").value,
+ "custom",
+ "The custom option is selected"
+ );
+ is(
+ helper.get("custom-margin-top").value,
+ "0.25",
+ "Top margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-right").value,
+ "1.00",
+ "Right margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-bottom").value,
+ "2.00",
+ "Bottom margin placeholder is correct"
+ );
+ is(
+ helper.get("custom-margin-left").value,
+ "0.00",
+ "Left margin placeholder is correct"
+ );
+ await helper.assertSettingsChanged(
+ { marginTop: 0.25, marginRight: 1, marginBottom: 2, marginLeft: 0 },
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ await helper.awaitAnimationFrame();
+
+ await helper.text(helper.get("custom-margin-top"), "0.50");
+ await helper.text(helper.get("custom-margin-right"), "0.50");
+ await helper.text(helper.get("custom-margin-bottom"), "0.50");
+ await helper.text(helper.get("custom-margin-left"), "0.50");
+
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ }
+ );
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testChangingBetweenMargins() {
+ await PrintHelper.withTestPage(async helper => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.printer_Mozilla_Save_to_PDF.print_margin_left", "1"]],
+ });
+
+ await helper.startPrint();
+ await helper.openMoreSettings();
+
+ let marginsPicker = helper.get("margins-picker");
+ is(marginsPicker.value, "custom", "First margin is custom");
+
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 1,
+ marginRight: 0.5,
+ });
+
+ info("Switch to Default margins");
+ await helper.assertSettingsChanged(
+ { marginLeft: 1 },
+ { marginLeft: 0.5 },
+ async () => {
+ let settingsChanged = helper.waitForSettingsEvent();
+ changeCustomToDefault(helper);
+ await settingsChanged;
+ }
+ );
+
+ is(marginsPicker.value, "default", "Default preset selected");
+
+ info("Switching back to Custom, should restore old margins");
+ await helper.assertSettingsChanged(
+ { marginLeft: 0.5 },
+ { marginLeft: 1 },
+ async () => {
+ let settingsChanged = helper.waitForSettingsEvent();
+ changeDefaultToCustom(helper);
+ await settingsChanged;
+ }
+ );
+
+ is(marginsPicker.value, "custom", "Custom is now selected");
+
+ info("Switching back to Default, should restore 0.5");
+ await helper.assertSettingsChanged(
+ { marginLeft: 1 },
+ { marginLeft: 0.5 },
+ async () => {
+ let settingsChanged = helper.waitForSettingsEvent();
+ changeCustomToDefault(helper);
+ await settingsChanged;
+ }
+ );
+
+ is(marginsPicker.value, "default", "Default preset is selected again");
+ });
+});
+
+add_task(async function testChangeHonoredInPrint() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+ await helper.setupMockPrint();
+
+ helper.mockFilePicker("changedMargin.pdf");
+
+ await helper.openMoreSettings();
+ helper.assertSettingsMatch({ marginRight: 0.5 });
+ this.changeDefaultToCustom(helper);
+
+ await helper.withClosingFn(async () => {
+ await helper.text(helper.get("custom-margin-right"), "1");
+ EventUtils.sendKey("return", helper.win);
+ helper.resolvePrint();
+ });
+ helper.assertPrintedWithSettings({ marginRight: 1 });
+ });
+});
+
+add_task(async function testInvalidPrefValueHeight() {
+ await PrintHelper.withTestPage(async helper => {
+ // Set some bad prefs
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.printer_Mozilla_Save_to_PDF.print_margin_top", "-1"]],
+ });
+ await helper.startPrint();
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ });
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testInvalidPrefValueWidth() {
+ await PrintHelper.withTestPage(async helper => {
+ // Set some bad prefs
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.printer_Mozilla_Save_to_PDF.print_margin_left", "-1"]],
+ });
+ await helper.startPrint();
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ });
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testInvalidMarginStartup() {
+ await PrintHelper.withTestPage(async helper => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.printer_Mozilla_Save_to_PDF.print_margin_right", "4"],
+ ["print.printer_Mozilla_Save_to_PDF.print_margin_left", "5"],
+ ],
+ });
+ await setupLetterPaper();
+ await helper.startPrint();
+ helper.assertSettingsMatch({
+ paperId: "na_letter",
+ marginLeft: 0.5,
+ marginRight: 0.5,
+ });
+ helper.closeDialog();
+ });
+});
+
+add_task(async function testRevalidateSwitchToNone() {
+ await PrintHelper.withTestPage(async helper => {
+ await setupLetterPaper();
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+
+ await helper.text(helper.get("custom-margin-bottom"), "6");
+ await helper.text(helper.get("custom-margin-top"), "6");
+ assertNoPendingMarginsUpdate(helper);
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ paperId: "na_letter",
+ });
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 6, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-bottom"), "3");
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ }
+ );
+
+ await helper.assertSettingsChanged(
+ { marginTop: 6, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 },
+ { marginTop: 0, marginRight: 0, marginBottom: 0, marginLeft: 0 },
+ async () => {
+ this.changeCustomToNone(helper);
+ is(
+ helper.get("margins-picker").value,
+ "none",
+ "No margins are now set"
+ );
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ }
+ );
+ });
+});
+
+add_task(async function testInvalidMarginResetAfterDestinationChange() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.printer_Fake_Printer.print_paper_id", "na_letter"],
+ ["print.printer_Fake_Printer.print_paper_size_unit", 0],
+ ["print.printer_Fake_Printer.print_paper_width", "8.5"],
+ ["print.printer_Fake_Printer.print_paper_height", "11"],
+ ],
+ });
+ await helper.startPrint();
+
+ let destinationPicker = helper.get("printer-picker");
+
+ await helper.openMoreSettings();
+ changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.assertSettingsNotChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ ok(marginError.hidden, "Margin error is hidden");
+
+ await helper.text(helper.get("custom-margin-top"), "20");
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError);
+
+ ok(!marginError.hidden, "Margin error is showing");
+ assertNoPendingMarginsUpdate(helper);
+ }
+ );
+
+ is(destinationPicker.disabled, false, "Destination picker is enabled");
+
+ helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await BrowserTestUtils.waitForCondition(
+ () => marginError.hidden,
+ "Wait for margin error to be hidden"
+ );
+
+ helper.assertSettingsMatch({
+ marginTop: 0.5,
+ marginRight: 0.5,
+ marginBottom: 0.5,
+ marginLeft: 0.5,
+ });
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testRevalidateCustomMarginsAfterPaperChanges() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ helper.dispatchSettingsChange({ paperId: "iso_a3" });
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 5, marginRight: 5, marginBottom: 5, marginLeft: 5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-top"), "5");
+ await helper.text(helper.get("custom-margin-bottom"), "5");
+ await helper.text(helper.get("custom-margin-right"), "5");
+ await helper.text(helper.get("custom-margin-left"), "5");
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+
+ await helper.assertSettingsChanged(
+ { marginTop: 5, marginRight: 5, marginBottom: 5, marginLeft: 5 },
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ helper.dispatchSettingsChange({ paperId: "iso_a5" });
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+ });
+});
+
+add_task(async function testRevalidateCustomMarginsAfterOrientationChanges() {
+ await PrintHelper.withTestPage(async helper => {
+ await setupLetterPaper();
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 5, marginRight: 0.5, marginBottom: 5, marginLeft: 0.5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-top"), "5");
+ await helper.text(helper.get("custom-margin-bottom"), "5");
+ assertPendingMarginsUpdate(helper);
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+
+ await helper.assertSettingsChanged(
+ { marginTop: 5, marginRight: 0.5, marginBottom: 5, marginLeft: 0.5 },
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ helper.dispatchSettingsChange({ orientation: 1 });
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+ });
+});
+
+add_task(async function testResetMarginPersists() {
+ await PrintHelper.withTestPage(async helper => {
+ await setupLetterPaper();
+ await helper.startPrint();
+
+ await helper.openMoreSettings();
+ this.changeDefaultToCustom(helper);
+ await helper.awaitAnimationFrame();
+ let marginError = helper.get("error-invalid-margin");
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ { marginTop: 0.5, marginRight: 4, marginBottom: 0.5, marginLeft: 4.5 },
+ async () => {
+ await helper.text(helper.get("custom-margin-right"), "4");
+ await helper.text(helper.get("custom-margin-left"), "4.5");
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+
+ await helper.assertSettingsChanged(
+ { marginTop: 0.5, marginRight: 4, marginBottom: 0.5, marginLeft: 4.5 },
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ helper.dispatchSettingsChange({ paperId: "iso_a4" });
+
+ // Wait for the preview to update, the margin options delay updates by
+ // INPUT_DELAY_MS, which is 500ms.
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+
+ await helper.assertSettingsNotChanged(
+ { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
+ async () => {
+ helper.dispatchSettingsChange({ paperId: "iso_a5" });
+ await helper.waitForSettingsEvent();
+ ok(marginError.hidden, "Margin error is hidden");
+ }
+ );
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_page_range.js b/toolkit/components/printing/tests/browser_print_page_range.js
new file mode 100644
index 0000000000..d5d64262c0
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_page_range.js
@@ -0,0 +1,471 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function changeAllToCustom(helper) {
+ let rangeSelect = helper.get("range-picker");
+ rangeSelect.focus();
+ rangeSelect.scrollIntoView({ block: "center" });
+ EventUtils.sendKey("space", helper.win);
+ EventUtils.sendKey("down", helper.win);
+ EventUtils.sendKey("return", helper.win);
+}
+
+function changeCustomToAll(helper) {
+ let rangeSelect = helper.get("range-picker");
+ rangeSelect.focus();
+ rangeSelect.scrollIntoView({ block: "center" });
+ EventUtils.sendKey("space", helper.win);
+ EventUtils.sendKey("up", helper.win);
+ EventUtils.sendKey("return", helper.win);
+}
+
+function getSheetCount(helper) {
+ return helper.doc.l10n.getAttributes(helper.get("sheet-count")).args
+ .sheetCount;
+}
+
+add_task(async function testRangeResetAfterScale() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+ await helper.setupMockPrint();
+
+ helper.mockFilePicker("changeRangeFromScale.pdf");
+ changeAllToCustom(helper);
+
+ await helper.openMoreSettings();
+ let scaleRadio = helper.get("percent-scale-choice");
+ await helper.waitForPreview(() => helper.click(scaleRadio));
+ let percentScale = helper.get("percent-scale");
+ await helper.waitForPreview(() => helper.text(percentScale, "200"));
+
+ let customRange = helper.get("custom-range");
+ let rangeError = helper.get("error-invalid-range");
+ await helper.waitForPreview(() => {
+ helper.text(customRange, "3");
+ });
+
+ ok(rangeError.hidden, "Range error is hidden");
+
+ await helper.text(percentScale, "10");
+ EventUtils.sendKey("return", helper.win);
+
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
+ ok(!rangeError.hidden, "Range error is showing");
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testRangeResetAfterPaperSize() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ paperId: "iso_a5" })
+ );
+ await helper.setupMockPrint();
+
+ await helper.openMoreSettings();
+ let scaleRadio = helper.get("percent-scale-choice");
+ await helper.waitForPreview(() => helper.click(scaleRadio));
+ let percentScale = helper.get("percent-scale");
+ await helper.waitForPreview(() => helper.text(percentScale, "200"));
+
+ let customRange = helper.get("custom-range");
+ changeAllToCustom(helper);
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange);
+
+ let rangeError = helper.get("error-invalid-range");
+ await helper.waitForPreview(() => {
+ helper.text(customRange, "6");
+ });
+
+ ok(rangeError.hidden, "Range error is hidden");
+
+ helper.dispatchSettingsChange({ paperId: "iso_a3" });
+ await BrowserTestUtils.waitForCondition(
+ () => helper.get("paper-size-picker").value == "iso_a3",
+ "Wait for paper size select to update"
+ );
+ EventUtils.sendKey("return", helper.win);
+
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
+ ok(!rangeError.hidden, "Range error is showing");
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testInvalidRangeResetAfterDestinationChange() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+
+ let destinationPicker = helper.get("printer-picker");
+ let customPageRange = helper.get("custom-range");
+
+ await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => {
+ changeAllToCustom(helper);
+ });
+ let rangeError = helper.get("error-invalid-range");
+
+ await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => {
+ ok(rangeError.hidden, "Range error is hidden");
+ await helper.text(customPageRange, "9");
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
+ ok(!rangeError.hidden, "Range error is showing");
+ });
+
+ is(destinationPicker.disabled, false, "Destination picker is enabled");
+
+ // Select a new printer
+ helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await BrowserTestUtils.waitForCondition(
+ () => rangeError.hidden,
+ "Wait for range error to be hidden"
+ );
+ is(customPageRange.value, "", "Page range has reset");
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testPageRangeSets() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let customRange = helper.get("custom-range");
+ let pageRangeInput = helper.get("page-range-input");
+ let invalidError = helper.get("error-invalid-range");
+ let invalidOverflowError = helper.get("error-invalid-start-range-overflow");
+
+ ok(customRange.hidden, "Custom range input is hidden");
+
+ changeAllToCustom(helper);
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange);
+
+ ok(!customRange.hidden, "Custom range is showing");
+
+ // We need to set the input to something to ensure we do not return early
+ // out of our validation function
+ helper.text(helper.get("custom-range"), ",");
+
+ let validStrings = {
+ "1": [1, 1],
+ "1,": [1, 1],
+ "2": [2, 2],
+ "1-2": [1, 2],
+ "1,2": [1, 2],
+ "1,2,": [1, 2],
+ "2,1": [1, 2],
+ "1,3": [1, 1, 3, 3],
+ "1-1,3": [1, 1, 3, 3],
+ "1,3-3": [1, 1, 3, 3],
+ "10-33": [10, 33],
+ "1-": [1, 50],
+ "-": [],
+ "-20": [1, 20],
+ "-,1": [],
+ "-1,1-": [],
+ "-1,1-2": [1, 2],
+ ",9": [9, 9],
+ ",": [],
+ "1,2,1,20,5": [1, 2, 5, 5, 20, 20],
+ "1-17,4,12-19": [1, 19],
+ "43-46,42,47-": [42, 50],
+ };
+
+ for (let [str, expected] of Object.entries(validStrings)) {
+ pageRangeInput._validateRangeInput(str, 50);
+ let pageRanges = pageRangeInput.formatPageRange();
+
+ is(
+ expected.every((page, index) => page === pageRanges[index]),
+ true,
+ `Expected page range for "${str}" matches "${expected}"`
+ );
+
+ ok(invalidError.hidden, "Generic error message is hidden");
+ ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
+ }
+
+ let invalidStrings = ["51", "1,51", "1-51", "4-1", "--", "0", "-90"];
+
+ for (let str of invalidStrings) {
+ pageRangeInput._validateRangeInput(str, 50);
+ is(pageRangeInput._pagesSet.size, 0, `There are no pages in the set`);
+ ok(!pageRangeInput.validity, "Input is invalid");
+ }
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testRangeError() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ changeAllToCustom(helper);
+
+ let invalidError = helper.get("error-invalid-range");
+ let invalidOverflowError = helper.get("error-invalid-start-range-overflow");
+
+ ok(invalidError.hidden, "Generic error message is hidden");
+ ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
+
+ helper.text(helper.get("custom-range"), "4");
+
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", invalidError);
+
+ ok(!invalidError.hidden, "Generic error message is showing");
+ ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testStartOverflowRangeError() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ changeAllToCustom(helper);
+
+ await helper.openMoreSettings();
+ let scaleRadio = helper.get("percent-scale-choice");
+ await helper.waitForPreview(() => helper.click(scaleRadio));
+ let percentScale = helper.get("percent-scale");
+ await helper.waitForPreview(() => helper.text(percentScale, "200"));
+
+ let invalidError = helper.get("error-invalid-range");
+ let invalidOverflowError = helper.get("error-invalid-start-range-overflow");
+
+ ok(invalidError.hidden, "Generic error message is hidden");
+ ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
+
+ helper.text(helper.get("custom-range"), "2-1");
+
+ await BrowserTestUtils.waitForAttributeRemoval(
+ "hidden",
+ invalidOverflowError
+ );
+
+ ok(invalidError.hidden, "Generic error message is hidden");
+ ok(!invalidOverflowError.hidden, "Start overflow error message is showing");
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testErrorClearedAfterSwitchingToAll() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ changeAllToCustom(helper);
+
+ let customRange = helper.get("custom-range");
+ let rangeError = helper.get("error-invalid-range");
+ ok(rangeError.hidden, "Generic error message is hidden");
+
+ helper.text(customRange, "3");
+
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
+ ok(!rangeError.hidden, "Generic error message is showing");
+
+ changeCustomToAll(helper);
+
+ await BrowserTestUtils.waitForCondition(
+ () => rangeError.hidden,
+ "Wait for range error to be hidden"
+ );
+ ok(customRange.hidden, "Custom range is hidden");
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testPageCountChangeNoRangeNoRerender() {
+ await PrintHelper.withTestPage(async helper => {
+ let customPrinter = "A printer";
+ helper.addMockPrinter(customPrinter);
+
+ await helper.startPrint();
+
+ await helper.assertSettingsChanged(
+ { printerName: "Mozilla Save to PDF" },
+ { printerName: customPrinter },
+ async () => {
+ let destinationPicker = helper.get("printer-picker");
+ destinationPicker.focus();
+ await Promise.all([
+ helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ printerName: customPrinter })
+ ),
+ helper.waitForSettingsEvent(),
+ ]);
+ }
+ );
+
+ // Change a setting that will change the number of pages. Since pageRanges
+ // is set to "all" then there shouldn't be a re-render because of it.
+ let previewUpdateCount = 0;
+ ok(!helper.hasPendingPreview, "No preview is pending");
+ helper.doc.addEventListener("preview-updated", () => previewUpdateCount++);
+
+ // Ensure the sheet count will change.
+ let initialSheetCount = getSheetCount(helper);
+
+ await helper.assertSettingsChanged(
+ { marginLeft: 0.5, marginRight: 0.5 },
+ { marginLeft: 3, marginRight: 3 },
+ async () => {
+ await Promise.all([
+ helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 })
+ ),
+ BrowserTestUtils.waitForEvent(helper.doc, "page-count"),
+ helper.waitForSettingsEvent(),
+ ]);
+ }
+ );
+
+ let newSheetCount = getSheetCount(helper);
+ ok(
+ initialSheetCount < newSheetCount,
+ `There are more sheets now ${initialSheetCount} < ${newSheetCount}`
+ );
+
+ ok(!helper.hasPendingPreview, "No preview is pending");
+ is(previewUpdateCount, 1, "Only one preview update fired");
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testPageCountChangeRangeNoRerender() {
+ await PrintHelper.withTestPage(async helper => {
+ let customPrinter = "A printer";
+ helper.addMockPrinter(customPrinter);
+
+ await helper.startPrint();
+
+ await helper.assertSettingsChanged(
+ { printerName: "Mozilla Save to PDF", pageRanges: [] },
+ { printerName: customPrinter, pageRanges: [1, 1] },
+ async () => {
+ let destinationPicker = helper.get("printer-picker");
+ destinationPicker.focus();
+ await Promise.all([
+ helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ printerName: customPrinter })
+ ),
+ helper.waitForSettingsEvent(),
+ ]);
+
+ await helper.waitForPreview(async () => {
+ changeAllToCustom(helper);
+ helper.text(helper.get("custom-range"), "1");
+ });
+ }
+ );
+
+ // Change a setting that will change the number of pages. Since pageRanges
+ // is set to a page that is in the new range, there shouldn't be a re-render.
+ let previewUpdateCount = 0;
+ ok(!helper.hasPendingPreview, "No preview is pending");
+ helper.doc.addEventListener("preview-updated", () => previewUpdateCount++);
+
+ await helper.assertSettingsChanged(
+ { marginLeft: 0.5, marginRight: 0.5 },
+ { marginLeft: 3, marginRight: 3 },
+ async () => {
+ await Promise.all([
+ helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 })
+ ),
+ BrowserTestUtils.waitForEvent(helper.doc, "page-count"),
+ helper.waitForSettingsEvent(),
+ ]);
+ }
+ );
+
+ let newSheetCount = getSheetCount(helper);
+ is(newSheetCount, 1, "There's still only one sheet");
+
+ ok(!helper.hasPendingPreview, "No preview is pending");
+ is(previewUpdateCount, 1, "Only one preview update fired");
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testPageCountChangeRangeRerender() {
+ await PrintHelper.withTestPage(async helper => {
+ let customPrinter = "A printer";
+ helper.addMockPrinter(customPrinter);
+
+ await helper.startPrint();
+
+ await helper.assertSettingsChanged(
+ { printerName: "Mozilla Save to PDF", pageRanges: [] },
+ { printerName: customPrinter, pageRanges: [1, 1] },
+ async () => {
+ let destinationPicker = helper.get("printer-picker");
+ destinationPicker.focus();
+ await Promise.all([
+ helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ printerName: customPrinter })
+ ),
+ helper.waitForSettingsEvent(),
+ ]);
+
+ await helper.waitForPreview(async () => {
+ changeAllToCustom(helper);
+ helper.text(helper.get("custom-range"), "1-");
+ });
+ }
+ );
+
+ // Change a setting that will change the number of pages. Since pageRanges
+ // is from 1-N the calculated page range will need to be updated.
+ let previewUpdateCount = 0;
+ ok(!helper.hasPendingPreview, "No preview is pending");
+ helper.doc.addEventListener("preview-updated", () => previewUpdateCount++);
+ let renderedTwice = BrowserTestUtils.waitForCondition(
+ () => previewUpdateCount == 2
+ );
+
+ // Ensure the sheet count will change.
+ let initialSheetCount = getSheetCount(helper);
+
+ await helper.assertSettingsChanged(
+ { marginLeft: 0.5, marginRight: 0.5 },
+ { marginLeft: 3, marginRight: 3 },
+ async () => {
+ await Promise.all([
+ helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 })
+ ),
+ BrowserTestUtils.waitForEvent(helper.doc, "page-count"),
+ helper.waitForSettingsEvent(),
+ ]);
+ await renderedTwice;
+ }
+ );
+
+ let newSheetCount = getSheetCount(helper);
+ ok(
+ initialSheetCount < newSheetCount,
+ `There are more sheets now ${initialSheetCount} < ${newSheetCount}`
+ );
+ Assert.deepEqual(
+ helper.viewSettings.pageRanges,
+ [1, newSheetCount],
+ "The new range is the updated full page range"
+ );
+
+ ok(!helper.hasPendingPreview, "No preview is pending");
+ is(previewUpdateCount, 2, "Preview updated again to show new page range");
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_paper_sizes.js b/toolkit/components/printing/tests/browser_print_paper_sizes.js
new file mode 100644
index 0000000000..6f81a2a0b4
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_paper_sizes.js
@@ -0,0 +1,120 @@
+"use strict";
+
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function selectPaperOptionWithValue(helper, value) {
+ let paperSelect = helper.get("paper-size-picker");
+ paperSelect.dispatchSettingsChange({
+ paperId: value,
+ });
+ await helper.awaitAnimationFrame();
+}
+
+add_task(async function testBadPaperSizeUnitCorrection() {
+ await PrintHelper.withTestPage(async helper => {
+ // Set prefs to select a non-default paper size
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_id", "na_letter"],
+ // paperSizeUnit is a bogus value, but the dimensions are correct for inches
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_size_unit", 99],
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_height", "11.0"],
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_width", "8.50"],
+ ],
+ });
+ await helper.startPrint();
+
+ let paperSelect = helper.get("paper-size-picker");
+ is(paperSelect.value, "na_letter", "The expected paper size is selected");
+ is(
+ helper.viewSettings.paperId,
+ "na_letter",
+ "The settings have the expected paperId"
+ );
+ is(
+ helper.viewSettings.paperSizeUnit,
+ helper.settings.kPaperSizeInches,
+ "Check paperSizeUnit"
+ );
+ is(helper.viewSettings.paperWidth.toFixed(1), "8.5", "Check paperWidth");
+ is(helper.viewSettings.paperHeight.toFixed(1), "11.0", "Check paperHeight");
+
+ await selectPaperOptionWithValue(helper, "iso_a3");
+ is(paperSelect.value, "iso_a3", "The expected paper size is selected");
+ is(
+ helper.viewSettings.paperId,
+ "iso_a3",
+ "The settings have the expected paperId"
+ );
+ is(
+ helper.viewSettings.paperSizeUnit,
+ helper.settings.kPaperSizeInches,
+ "Check paperSizeUnit"
+ );
+ is(helper.viewSettings.paperWidth.toFixed(1), "11.7", "Check paperWidth");
+ is(helper.viewSettings.paperHeight.toFixed(1), "16.5", "Check paperHeight");
+
+ await SpecialPowers.popPrefEnv();
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testMismatchedPaperSizeUnitCorrection() {
+ await PrintHelper.withTestPage(async helper => {
+ // Set prefs to select a non-default paper size
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_id", "na_ledger"],
+ // paperSizeUnit is millimeters, but the dimensions are correct for inches
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_size_unit", 1],
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_width", "11.0"],
+ ["print.printer_Mozilla_Save_to_PDF.print_paper_height", "17.0"],
+ ],
+ });
+ await helper.startPrint();
+
+ let paperSelect = helper.get("paper-size-picker");
+ is(paperSelect.value, "na_ledger", "The expected paper size is selected");
+
+ // We expect to honor the paperSizeUnit, and convert paperWidth/Height to that unit
+ is(
+ helper.viewSettings.paperId,
+ "na_ledger",
+ "The settings have the expected paperId"
+ );
+ is(
+ helper.viewSettings.paperSizeUnit,
+ helper.settings.kPaperSizeMillimeters,
+ "Check paperSizeUnit"
+ );
+ is(helper.viewSettings.paperWidth.toFixed(1), "279.4", "Check paperWidth");
+ is(
+ helper.viewSettings.paperHeight.toFixed(1),
+ "431.8",
+ "Check paperHeight"
+ );
+
+ await selectPaperOptionWithValue(helper, "iso_a3");
+ is(paperSelect.value, "iso_a3", "The expected paper size is selected");
+ is(
+ helper.viewSettings.paperId,
+ "iso_a3",
+ "The settings have the expected paperId"
+ );
+ is(
+ helper.viewSettings.paperSizeUnit,
+ helper.settings.kPaperSizeMillimeters,
+ "Check paperSizeUnit"
+ );
+ is(helper.viewSettings.paperWidth.toFixed(1), "297.0", "Check paperWidth");
+ is(
+ helper.viewSettings.paperHeight.toFixed(1),
+ "420.0",
+ "Check paperHeight"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_scaling.js b/toolkit/components/printing/tests/browser_print_scaling.js
new file mode 100644
index 0000000000..85b8a68449
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_scaling.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testInvalidScaleResetAfterDestinationChange() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+
+ let destinationPicker = helper.get("printer-picker");
+
+ await helper.openMoreSettings();
+ let scaleRadio = helper.get("percent-scale-choice");
+ await helper.assertSettingsChanged(
+ { shrinkToFit: true },
+ { shrinkToFit: false },
+ async () => {
+ await helper.waitForPreview(() => helper.click(scaleRadio));
+ }
+ );
+ let percentScale = helper.get("percent-scale");
+
+ let scaleError = helper.get("error-invalid-scale");
+
+ await helper.assertSettingsNotChanged({ scaling: 1 }, async () => {
+ ok(scaleError.hidden, "Scale error is hidden");
+ await helper.text(percentScale, "9");
+ await BrowserTestUtils.waitForAttributeRemoval("hidden", scaleError);
+ ok(!scaleError.hidden, "Scale error is showing");
+ });
+
+ is(destinationPicker.disabled, false, "Destination picker is enabled");
+
+ // Select a new printer
+ await helper.dispatchSettingsChange({ printerName: mockPrinterName });
+ await BrowserTestUtils.waitForCondition(
+ () => scaleError.hidden,
+ "Wait for scale error to be hidden"
+ );
+ is(percentScale.value, "100", "Scale has reset to 100");
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_print_selection.js b/toolkit/components/printing/tests/browser_print_selection.js
new file mode 100644
index 0000000000..fddc599788
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_print_selection.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const frameSource =
+ "<a href='about:mozilla'>some text</a><a id='other' href='about:about'>other text</a>";
+const sources = [
+ `<html><iframe id="f" srcdoc="${frameSource}"></iframe></html>`,
+ `<html><iframe id="f" src="https://example.com/document-builder.sjs?html=${frameSource}"></iframe></html>`,
+];
+
+async function getPreviewText(previewBrowser) {
+ return SpecialPowers.spawn(previewBrowser, [], function() {
+ return content.document.body.textContent;
+ });
+}
+
+add_task(async function print_selection() {
+ let i = 0;
+ for (let source of sources) {
+ // Testing the native print dialog is much harder.
+ // Note we need to do this from here since resetPrintPrefs() below clears
+ // out the pref.
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "data:text/html," + source,
+ async function(browser) {
+ let frameBC = browser.browsingContext.children[0];
+ await SpecialPowers.spawn(frameBC, [], () => {
+ let element = content.document.getElementById("other");
+ content.focus();
+ content.getSelection().selectAllChildren(element);
+ });
+
+ let helper = new PrintHelper(browser);
+
+ // If you change this, change nsContextMenu.printSelection() too.
+ PrintUtils.startPrintWindow("tests", frameBC, {
+ printSelectionOnly: true,
+ });
+
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ let previewBrowser = document.querySelector(
+ ".printPreviewBrowser[previewtype='selection']"
+ );
+ let previewText = () => getPreviewText(previewBrowser);
+ // The preview process is async, wait for it to not be empty.
+ let textContent = await TestUtils.waitForCondition(previewText);
+ is(textContent, "other text", "Correct content loaded");
+
+ let printSelect = document
+ .querySelector(".printSettingsBrowser")
+ .contentDocument.querySelector("#print-selection-enabled");
+ ok(!printSelect.hidden, "Print selection checkbox is shown");
+ ok(printSelect.checked, "Print selection checkbox is checked");
+
+ let file = helper.mockFilePicker(`browser_print_selection-${i++}.pdf`);
+ await helper.assertPrintToFile(file, () => {
+ helper.click(helper.get("print-button"));
+ });
+ PrintHelper.resetPrintPrefs();
+ }
+ );
+ }
+});
+
+add_task(async function no_print_selection() {
+ // Ensures the print selection checkbox is hidden if nothing is selected
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.openMoreSettings();
+
+ let printSelect = helper.get("print-selection-container");
+ ok(printSelect.hidden, "Print selection checkbox is hidden");
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function print_selection_switch() {
+ await PrintHelper.withTestPage(async helper => {
+ await SpecialPowers.spawn(helper.sourceBrowser, [], async function() {
+ let element = content.document.querySelector("h1");
+ content.window.getSelection().selectAllChildren(element);
+ });
+
+ await helper.startPrint();
+ await helper.openMoreSettings();
+ let printSelect = helper.get("print-selection-container");
+ ok(!printSelect.checked, "Print selection checkbox is not checked");
+
+ let selectionBrowser = document.querySelector(
+ ".printPreviewBrowser[previewtype='selection']"
+ );
+ let primaryBrowser = document.querySelector(
+ ".printPreviewBrowser[previewtype='primary']"
+ );
+
+ let selectedText = "Article title";
+ let fullText = await getPreviewText(primaryBrowser);
+
+ function getCurrentBrowser(previewType) {
+ let browser =
+ previewType == "selection" ? selectionBrowser : primaryBrowser;
+ is(
+ browser.parentElement.getAttribute("previewtype"),
+ previewType,
+ "Expected browser is showing"
+ );
+ return browser;
+ }
+
+ helper.assertSettingsMatch({
+ printSelectionOnly: false,
+ });
+
+ is(
+ selectionBrowser.parentElement.getAttribute("previewtype"),
+ "primary",
+ "Print selection browser is not shown"
+ );
+
+ await helper.assertSettingsChanged(
+ { printSelectionOnly: false },
+ { printSelectionOnly: true },
+ async () => {
+ await helper.waitForPreview(() => helper.click(printSelect));
+ let text = await getPreviewText(getCurrentBrowser("selection"));
+ is(text, selectedText, "Correct content loaded");
+ }
+ );
+
+ await helper.assertSettingsChanged(
+ { printSelectionOnly: true },
+ { printSelectionOnly: false },
+ async () => {
+ await helper.waitForPreview(() => helper.click(printSelect));
+ let previewType = selectionBrowser.parentElement.getAttribute(
+ "previewtype"
+ );
+ is(previewType, "primary", "Print selection browser is not shown");
+ let text = await getPreviewText(getCurrentBrowser(previewType));
+ is(text, fullText, "Correct content loaded");
+ }
+ );
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_sheet_count.js b/toolkit/components/printing/tests/browser_sheet_count.js
new file mode 100644
index 0000000000..f53ba5ba80
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_sheet_count.js
@@ -0,0 +1,228 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function getSheetCount(el) {
+ return el.ownerDocument.l10n.getAttributes(el).args.sheetCount;
+}
+
+add_task(async function testSheetCount() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let sheetCount = helper.get("sheet-count");
+ let { id } = helper.doc.l10n.getAttributes(sheetCount);
+ is(id, "printui-sheets-count", "The l10n id is correct");
+ let initialSheetCount = getSheetCount(sheetCount);
+ ok(initialSheetCount >= 1, "There is an initial sheet count");
+
+ await helper.openMoreSettings();
+
+ let scaleRadio = helper.get("percent-scale-choice");
+ await helper.waitForPreview(() => helper.click(scaleRadio));
+
+ let percentScale = helper.get("percent-scale");
+ await helper.waitForPreview(() => helper.text(percentScale, "200"));
+
+ let zoomedSheetCount = getSheetCount(sheetCount);
+ ok(zoomedSheetCount > initialSheetCount, "The sheet count increased");
+
+ // Since we're using the Save to PDF printer, the numCopies element should
+ // be hidden and its value ignored.
+ let numCopies = helper.get("copies-count");
+ ok(BrowserTestUtils.is_hidden(numCopies), "numCopies element is hidden");
+ helper.dispatchSettingsChange({
+ numCopies: 4,
+ });
+ is(
+ getSheetCount(sheetCount),
+ zoomedSheetCount,
+ "numCopies is ignored for Save to PDF printer"
+ );
+
+ is(helper.viewSettings.numCopies, 1, "numCopies is 1 in viewSettings");
+
+ // We don't have any "real" printers set up for testing yet, so insert a modified
+ // copy of the PDF printer which pretends to be real, and switch to that
+ // to triggers the component to update.
+ let realPrinterName = "My real printer";
+ let pdfPrinterInfo =
+ helper.win.PrintSettingsViewProxy.availablePrinters[
+ PrintUtils.SAVE_TO_PDF_PRINTER
+ ];
+ let mockPrinterInfo = Object.assign({}, pdfPrinterInfo, {});
+ mockPrinterInfo.settings = pdfPrinterInfo.settings.clone();
+ mockPrinterInfo.settings.outputFormat =
+ Ci.nsIPrintSettings.kOutputFormatNative;
+ mockPrinterInfo.settings.printerName = realPrinterName;
+
+ helper.win.PrintSettingsViewProxy.availablePrinters[
+ realPrinterName
+ ] = mockPrinterInfo;
+ await helper.dispatchSettingsChange({
+ printerName: realPrinterName,
+ });
+ await helper.awaitAnimationFrame();
+
+ let { settings, viewSettings } = helper;
+
+ is(
+ settings.printerName,
+ realPrinterName,
+ "Sanity check the current settings have the new printerName"
+ );
+ is(
+ settings.outputFormat,
+ Ci.nsIPrintSettings.kOutputFormatNative,
+ "The new printer has the correct outputFormat"
+ );
+ is(viewSettings.numCopies, 4, "numCopies is 4 in viewSettings");
+
+ // numCopies is now visible and sheetCount is multiplied by numCopies.
+ ok(BrowserTestUtils.is_visible(numCopies), "numCopies element is visible");
+ is(numCopies.value, "4", "numCopies displays the correct value");
+ is(
+ getSheetCount(sheetCount),
+ zoomedSheetCount * 4,
+ "numCopies is used when using a non-PDF printer"
+ );
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testSheetCountPageRange() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+ await helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({
+ shrinkToFit: false,
+ scaling: 2,
+ })
+ );
+
+ let sheetCount = helper.get("sheet-count");
+ await BrowserTestUtils.waitForCondition(
+ () => getSheetCount(sheetCount) != 1,
+ "Wait for sheet count to update"
+ );
+ let sheets = getSheetCount(sheetCount);
+ ok(sheets >= 3, "There are at least 3 pages");
+
+ // Set page range to 2-3, sheet count should be 2.
+ await helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({
+ pageRanges: [2, 3],
+ })
+ );
+
+ sheets = getSheetCount(sheetCount);
+ is(sheets, 2, "There are now only 2 pages shown");
+ });
+});
+
+add_task(async function testPagesPerSheetCount() {
+ await PrintHelper.withTestPage(async helper => {
+ let mockPrinterName = "A real printer!";
+ helper.addMockPrinter(mockPrinterName);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["print.pages_per_sheet.enabled", true],
+ ["print_printer", mockPrinterName],
+ ],
+ });
+
+ await helper.startPrint();
+
+ await helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({
+ shrinkToFit: false,
+ scaling: 2,
+ })
+ );
+
+ let sheetCount = helper.get("sheet-count");
+ await BrowserTestUtils.waitForCondition(
+ () => getSheetCount(sheetCount) != 1,
+ "Wait for sheet count to update"
+ );
+ let sheets = getSheetCount(sheetCount);
+
+ ok(sheets > 1, "There are multiple pages");
+
+ await helper.openMoreSettings();
+ let pagesPerSheet = helper.get("pages-per-sheet-picker");
+ ok(BrowserTestUtils.is_visible(pagesPerSheet), "Pages per sheet is shown");
+ pagesPerSheet.focus();
+ EventUtils.sendKey("space", helper.win);
+ for (let i = 0; i < 7; i++) {
+ EventUtils.sendKey("down", helper.win);
+ if (pagesPerSheet.value == 16) {
+ break;
+ }
+ }
+ await helper.waitForPreview(() => EventUtils.sendKey("return", helper.win));
+
+ sheets = getSheetCount(sheetCount);
+ is(sheets, 1, "There's only one sheet now");
+
+ await helper.waitForSettingsEvent(() =>
+ helper.dispatchSettingsChange({ numCopies: 5 })
+ );
+
+ sheets = getSheetCount(sheetCount);
+ is(sheets, 5, "Copies are handled with pages per sheet correctly");
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testPagesPerSheetPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.pages_per_sheet.enabled", false]],
+ });
+
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ ok(
+ BrowserTestUtils.is_hidden(helper.get("pages-per-sheet")),
+ "Pages per sheet is hidden"
+ );
+
+ await helper.closeDialog();
+ });
+});
+
+add_task(async function testUpdateCopiesNoPreviewUpdate() {
+ const mockPrinterName = "Fake Printer";
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter(mockPrinterName);
+ await helper.startPrint();
+
+ await helper.waitForSettingsEvent(() =>
+ helper.dispatchSettingsChange({ numCopies: 5 })
+ );
+
+ ok(
+ !helper.win.PrintEventHandler._updatePrintPreviewTask.isArmed,
+ "Preview Task is not armed"
+ );
+
+ await helper.waitForPreview(() =>
+ helper.dispatchSettingsChange({ printerName: mockPrinterName })
+ );
+
+ await helper.waitForSettingsEvent(() =>
+ helper.dispatchSettingsChange({ numCopies: 2 })
+ );
+ ok(
+ !helper.win.PrintEventHandler._updatePrintPreviewTask.isArmed,
+ "Preview Task is not armed"
+ );
+
+ await helper.closeDialog();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js b/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js
new file mode 100644
index 0000000000..d5b8b5cfe6
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function testSystemDialogLinkState() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ is(
+ helper.get("printer-picker").options.length,
+ 1,
+ "Only the Save to PDF printer is available"
+ );
+
+ let systemLink = helper.get("open-dialog-link");
+ if (AppConstants.platform == "win") {
+ ok(
+ BrowserTestUtils.is_hidden(systemLink),
+ "Link is hidden on Windows with no extra printers"
+ );
+ } else {
+ ok(
+ BrowserTestUtils.is_visible(systemLink),
+ "Link is visible on Linux/macOS"
+ );
+ }
+ });
+});
+
+add_task(async function testModalPrintDialog() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter("A printer");
+ await SpecialPowers.pushPrefEnv({
+ set: [["print_printer", "A printer"]],
+ });
+
+ await helper.startPrint();
+
+ helper.assertDialogOpen();
+
+ helper.assertSettingsMatch({ printerName: "A printer" });
+ await helper.setupMockPrint();
+
+ helper.click(helper.get("open-dialog-link"));
+
+ helper.assertDialogHidden();
+
+ await helper.withClosingFn(() => {
+ helper.resolveShowSystemDialog();
+ helper.resolvePrint();
+ });
+
+ helper.assertDialogClosed();
+ });
+});
+
+add_task(async function testModalPrintDialogCancelled() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter("A printer");
+ await SpecialPowers.pushPrefEnv({
+ set: [["print_printer", "A printer"]],
+ });
+
+ await helper.startPrint();
+
+ helper.assertDialogOpen();
+
+ helper.assertSettingsMatch({ printerName: "A printer" });
+ await helper.setupMockPrint();
+
+ helper.click(helper.get("open-dialog-link"));
+
+ helper.assertDialogHidden();
+
+ await helper.withClosingFn(() => {
+ helper.rejectShowSystemDialog();
+ });
+
+ helper.assertDialogClosed();
+ });
+});
+
+add_task(async function testPrintDoesNotWaitForPreview() {
+ await PrintHelper.withTestPage(async helper => {
+ helper.addMockPrinter("A printer");
+ await SpecialPowers.pushPrefEnv({
+ set: [["print_printer", "A printer"]],
+ });
+
+ await helper.startPrint({ waitFor: "loadComplete" });
+ await helper.awaitAnimationFrame();
+
+ helper.mockFilePicker("print_does_not_wait_for_preview.pdf");
+ await helper.setupMockPrint();
+ helper.click(helper.get("open-dialog-link"));
+
+ helper.assertDialogHidden();
+ await helper.withClosingFn(() => {
+ helper.resolveShowSystemDialog();
+ helper.resolvePrint();
+ });
+
+ helper.assertDialogClosed();
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_ui_labels.js b/toolkit/components/printing/tests/browser_ui_labels.js
new file mode 100644
index 0000000000..a107bac4d1
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_ui_labels.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_FormFieldLabels() {
+ await PrintHelper.withTestPage(async helper => {
+ await helper.startPrint();
+
+ let fields = Array.from(helper.get("print").elements);
+ for (let field of fields) {
+ if (field.localName == "button") {
+ continue;
+ }
+ ok(
+ field.labels.length ||
+ field.hasAttribute("aria-label") ||
+ field.hasAttribute("aria-labelledby"),
+ `Field ${field.localName}#${field.id} should be labelled`
+ );
+ }
+ });
+});
diff --git a/toolkit/components/printing/tests/browser_window_print.js b/toolkit/components/printing/tests/browser_window_print.js
new file mode 100644
index 0000000000..49a4485ef1
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_window_print.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(async function test_print_blocks() {
+ // window.print() only shows print preview when print.tab_modal.enabled is
+ // true.
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ `${TEST_PATH}file_window_print.html`,
+ async function(browser) {
+ info(
+ "Waiting for the first window.print() to run and ensure we're showing the preview..."
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ {
+ let [before, afterFirst] = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return [
+ !!content.document.getElementById("before-print"),
+ !!content.document.getElementById("after-first-print"),
+ ];
+ }
+ );
+
+ ok(before, "Content before printing should be in the DOM");
+ ok(!afterFirst, "Shouldn't have returned yet from window.print()");
+ }
+
+ gBrowser.getTabDialogBox(browser).abortAllDialogs();
+
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ {
+ let [before, afterFirst, afterSecond] = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => {
+ return [
+ !!content.document.getElementById("before-print"),
+ !!content.document.getElementById("after-first-print"),
+ !!content.document.getElementById("after-second-print"),
+ ];
+ }
+ );
+
+ ok(before, "Content before printing should be in the DOM");
+ ok(afterFirst, "Should be in the second print already");
+ ok(afterSecond, "Shouldn't have blocked if we have mozPrintCallbacks");
+ }
+ }
+ );
+});
+
+add_task(async function test_print_delayed_during_load() {
+ // window.print() only shows print preview when print.tab_modal.enabled is
+ // true.
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ `${TEST_PATH}file_window_print_delayed_during_load.html`,
+ async function(browser) {
+ info(
+ "Waiting for the first window.print() to run and ensure we're showing the preview..."
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ // The print dialog is open, should be open after onload.
+ {
+ let duringLoad = await SpecialPowers.spawn(browser, [], () => {
+ return !!content.document.getElementById("added-during-load");
+ });
+ ok(duringLoad, "Print should've been delayed");
+ }
+
+ gBrowser.getTabDialogBox(browser).abortAllDialogs();
+
+ is(typeof browser.isConnected, "boolean");
+ await BrowserTestUtils.waitForCondition(() => !browser.isConnected);
+ ok(true, "Tab should've been closed after printing");
+ }
+ );
+});
+
+add_task(async function test_print_on_sandboxed_frame() {
+ // window.print() only shows print preview when print.tab_modal.enabled is
+ // true.
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ `${TEST_PATH}file_window_print_sandboxed_iframe.html`,
+ async function(browser) {
+ info(
+ "Waiting for the first window.print() to run and ensure we're showing the preview..."
+ );
+
+ await BrowserTestUtils.waitForCondition(
+ () => !!document.querySelector(".printPreviewBrowser")
+ );
+
+ isnot(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "Should open the print preview correctly"
+ );
+ gBrowser.getTabDialogBox(browser).abortAllDialogs();
+ }
+ );
+});
+
+add_task(async function test_focused_browsing_context() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `${TEST_PATH}longerArticle.html`
+ );
+
+ let tabCount = gBrowser.tabs.length;
+ document.getElementById("cmd_newNavigatorTab").doCommand();
+ await TestUtils.waitForCondition(() => gBrowser.tabs.length == tabCount + 1);
+ let newTabBrowser = gBrowser.selectedBrowser;
+ is(newTabBrowser.documentURI.spec, "about:newtab", "newtab is loaded");
+
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ menuButton.click();
+ await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown");
+ document.getElementById("appMenu-print-button").click();
+
+ let dialog = await TestUtils.waitForCondition(
+ () =>
+ gBrowser
+ .getTabDialogBox(newTabBrowser)
+ .getTabDialogManager()
+ ._dialogs.find(dlg => dlg._box.querySelector(".printSettingsBrowser")),
+ "Wait for dialog"
+ );
+ await dialog._dialogReady;
+ ok(dialog, "Dialog is available");
+ await dialog._frame.contentWindow._initialized;
+ await dialog.close();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/toolkit/components/printing/tests/file_coop_header.html b/toolkit/components/printing/tests/file_coop_header.html
new file mode 100644
index 0000000000..22eb518ba0
--- /dev/null
+++ b/toolkit/components/printing/tests/file_coop_header.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <p>Hello world</p>
+ </body>
+</html>
diff --git a/toolkit/components/printing/tests/file_coop_header.html^headers^ b/toolkit/components/printing/tests/file_coop_header.html^headers^
new file mode 100644
index 0000000000..46ad58d83b
--- /dev/null
+++ b/toolkit/components/printing/tests/file_coop_header.html^headers^
@@ -0,0 +1 @@
+Cross-Origin-Opener-Policy: same-origin
diff --git a/toolkit/components/printing/tests/file_page_change_print_original_1.html b/toolkit/components/printing/tests/file_page_change_print_original_1.html
new file mode 100644
index 0000000000..c567b746c9
--- /dev/null
+++ b/toolkit/components/printing/tests/file_page_change_print_original_1.html
@@ -0,0 +1,8 @@
+<script>
+window.onafterprint = function() {
+ setTimeout(function() {
+ window.location = "file_page_change_print_original_2.html";
+ }, 0);
+};
+</script>
+<pre>INITIAL PAGE</pre>
diff --git a/toolkit/components/printing/tests/file_page_change_print_original_2.html b/toolkit/components/printing/tests/file_page_change_print_original_2.html
new file mode 100644
index 0000000000..44f33281c9
--- /dev/null
+++ b/toolkit/components/printing/tests/file_page_change_print_original_2.html
@@ -0,0 +1 @@
+REPLACED PAGE!
diff --git a/toolkit/components/printing/tests/file_pdf.pdf b/toolkit/components/printing/tests/file_pdf.pdf
new file mode 100644
index 0000000000..593558f9a4
--- /dev/null
+++ b/toolkit/components/printing/tests/file_pdf.pdf
@@ -0,0 +1,12 @@
+%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000102 00000 n
+trailer<</Size 4/Root 1 0 R>>
+startxref
+149
+%EOF \ No newline at end of file
diff --git a/toolkit/components/printing/tests/file_print.html b/toolkit/components/printing/tests/file_print.html
new file mode 100644
index 0000000000..be7de08dbd
--- /dev/null
+++ b/toolkit/components/printing/tests/file_print.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+ window.print();
+</script>
+<p id="printed">I should be printed</p>
diff --git a/toolkit/components/printing/tests/file_window_print.html b/toolkit/components/printing/tests/file_window_print.html
new file mode 100644
index 0000000000..6b18a04cca
--- /dev/null
+++ b/toolkit/components/printing/tests/file_window_print.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<div id="before-print">Before print</div>
+<canvas id="canvas" width="100" height="100"></canvas>
+<script>
+ onload = function() {
+ // window.print() is special until after the load event is finished firing.
+ setTimeout(function() {
+ // This fires a timer which would trigger a navigation and prevent the
+ // test from completing if it happens during window.print().
+ let meta = document.createElement("meta");
+ meta.setAttribute("http-equiv", "refresh");
+ meta.setAttribute("content", "0; url=/unlikely-to-be-found");
+ document.head.appendChild(meta);
+ // This one should block until we're done printing, and block the
+ // navigation too.
+ window.print();
+ meta.remove();
+ document.body.insertAdjacentHTML('beforeend', `<div id="after-first-print">After first print</div>`);
+
+ let canvas = document.getElementById("canvas");
+ canvas.mozPrintCallback = function() {};
+
+ // This one shouldn't, because the print callbacks need to run.
+ window.print();
+
+ document.body.insertAdjacentHTML('beforeend', `<div id="after-second-print">After second print</div>`);
+ }, 0);
+ }
+</script>
diff --git a/toolkit/components/printing/tests/file_window_print_delayed_during_load.html b/toolkit/components/printing/tests/file_window_print_delayed_during_load.html
new file mode 100644
index 0000000000..4ac5ed9a8a
--- /dev/null
+++ b/toolkit/components/printing/tests/file_window_print_delayed_during_load.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<body>
+<script>
+ onload = function() {
+ let div = document.createElement("div");
+ div.id = "added-during-load";
+ div.innerHTML = "I should be printed";
+ document.body.appendChild(div);
+ };
+
+ window.print(); // This should be delayed until after load.
+ window.close(); // So should this.
+</script>
diff --git a/toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html b/toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html
new file mode 100644
index 0000000000..a8e832932c
--- /dev/null
+++ b/toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<iframe sandbox="allow-same-origin allow-scripts allow-modals" src="about:blank" width="0" height="0"></iframe>
+<script>
+ onload = function() {
+ document.querySelector("iframe").contentWindow.print();
+ };
+</script>
diff --git a/toolkit/components/printing/tests/head.js b/toolkit/components/printing/tests/head.js
new file mode 100644
index 0000000000..2629e7d780
--- /dev/null
+++ b/toolkit/components/printing/tests/head.js
@@ -0,0 +1,444 @@
+const PRINT_DOCUMENT_URI = "chrome://global/content/print.html";
+const { MockFilePicker } = SpecialPowers;
+
+let pickerMocked = false;
+
+class PrintHelper {
+ static async withTestPage(testFn, pagePathname) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["print.tab_modal.enabled", true]],
+ });
+
+ let pageUrl = pagePathname
+ ? this.getTestPageUrl(pagePathname)
+ : this.defaultTestPageUrl;
+ info("withTestPage: " + pageUrl);
+ let isPdf = pageUrl.endsWith(".pdf");
+
+ if (isPdf) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["pdfjs.eventBusDispatchToDOM", true]],
+ });
+ }
+
+ let taskReturn = await BrowserTestUtils.withNewTab(
+ isPdf ? "about:blank" : pageUrl,
+ async function(browser) {
+ if (isPdf) {
+ let loaded = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+ await SpecialPowers.spawn(browser, [pageUrl], contentUrl => {
+ content.location = contentUrl;
+ });
+ await loaded;
+ }
+ await testFn(new PrintHelper(browser));
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+ if (isPdf) {
+ await SpecialPowers.popPrefEnv();
+ }
+
+ // Reset all of the other printing prefs to their default.
+ this.resetPrintPrefs();
+ return taskReturn;
+ }
+
+ static resetPrintPrefs() {
+ for (let name of Services.prefs.getChildList("print.")) {
+ Services.prefs.clearUserPref(name);
+ }
+ Services.prefs.clearUserPref("print_printer");
+ }
+
+ static getTestPageUrl(pathName) {
+ const testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ );
+ return testPath + pathName;
+ }
+
+ static get defaultTestPageUrl() {
+ return this.getTestPageUrl("simplifyArticleSample.html");
+ }
+
+ static createMockPaper(paperProperties = {}) {
+ return Object.assign(
+ {
+ id: "regular",
+ name: "Regular Size",
+ width: 612,
+ height: 792,
+ unwriteableMargin: {
+ marginTop: 0.1,
+ marginBottom: 0.1,
+ marginLeft: 0.1,
+ marginRight: 0.1,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPaperMargin]),
+ },
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPaper]),
+ },
+ paperProperties
+ );
+ }
+
+ // This is used only for the old print preview. For tests
+ // involving the newer UI, use waitForPreview instead.
+ static waitForOldPrintPreview(expectedBrowser) {
+ const { PrintingParent } = ChromeUtils.import(
+ "resource://gre/actors/PrintingParent.jsm"
+ );
+
+ return new Promise(resolve => {
+ PrintingParent.setTestListener(browser => {
+ if (browser == expectedBrowser) {
+ PrintingParent.setTestListener(null);
+ resolve();
+ }
+ });
+ });
+ }
+
+ constructor(sourceBrowser) {
+ this.sourceBrowser = sourceBrowser;
+ }
+
+ async startPrint(condition = {}) {
+ this.sourceBrowser.ownerGlobal.document
+ .getElementById("cmd_print")
+ .doCommand();
+ let dialog = await TestUtils.waitForCondition(
+ () => this.dialog,
+ "Wait for dialog"
+ );
+ await dialog._dialogReady;
+
+ if (Object.keys(condition).length === 0) {
+ await this.win._initialized;
+ } else if (condition.waitFor == "loadComplete") {
+ await BrowserTestUtils.waitForAttributeRemoval("loading", document.body);
+ }
+ }
+
+ beforeInit(initFn) {
+ // Run a function when the print.html document is created,
+ // but before its init is called from the domcontentloaded handler
+ TestUtils.topicObserved("document-element-inserted", doc => {
+ return (
+ doc.nodePrincipal.isSystemPrincipal &&
+ doc.contentType == "text/html" &&
+ doc.URL.startsWith("chrome://global/content/print.html")
+ );
+ }).then(([doc]) => {
+ doc.addEventListener("DOMContentLoaded", () => {
+ initFn(doc.ownerGlobal);
+ });
+ });
+ }
+
+ async withClosingFn(closeFn) {
+ let { dialog } = this;
+ await closeFn();
+ if (this.dialog) {
+ await TestUtils.waitForCondition(
+ () => !this.dialog,
+ "Wait for dialog to close"
+ );
+ }
+ await dialog._closingPromise;
+ }
+
+ resetSettings() {
+ this.win.PrintEventHandler.settings = this.win.PrintEventHandler.defaultSettings;
+ this.win.PrintEventHandler.saveSettingsToPrefs(
+ this.win.PrintEventHandler.kInitSaveAll
+ );
+ }
+
+ async closeDialog() {
+ this.resetSettings();
+ await this.withClosingFn(() => this.dialog.close());
+ }
+
+ assertDialogClosed() {
+ is(this._dialogs.length, 0, "There are no print dialogs");
+ }
+
+ assertDialogOpen() {
+ is(this._dialogs.length, 1, "There is one print dialog");
+ ok(BrowserTestUtils.is_visible(this.dialog._box), "The dialog is visible");
+ }
+
+ assertDialogHidden() {
+ is(this._dialogs.length, 1, "There is one print dialog");
+ ok(BrowserTestUtils.is_hidden(this.dialog._box), "The dialog is hidden");
+ ok(
+ this.dialog._box.getBoundingClientRect().width > 0,
+ "The dialog should still have boxes"
+ );
+ }
+
+ async assertPrintToFile(file, testFn) {
+ ok(!file.exists(), "File does not exist before printing");
+ await this.withClosingFn(testFn);
+ await TestUtils.waitForCondition(
+ () => file.exists() && file.fileSize > 0,
+ "Wait for target file to get created",
+ 50
+ );
+ ok(file.exists(), "Created target file");
+
+ await TestUtils.waitForCondition(
+ () => file.fileSize > 0,
+ "Wait for the print progress to run",
+ 50
+ );
+
+ ok(file.fileSize > 0, "Target file not empty");
+ }
+
+ setupMockPrint() {
+ if (this.resolveShowSystemDialog) {
+ throw new Error("Print already mocked");
+ }
+
+ // Create some Promises that we can resolve/reject from the test.
+ let showSystemDialogPromise = new Promise((resolve, reject) => {
+ this.resolveShowSystemDialog = resolve;
+ this.rejectShowSystemDialog = () => {
+ reject(Components.Exception("", Cr.NS_ERROR_ABORT));
+ };
+ });
+ let printPromise = new Promise((resolve, reject) => {
+ this.resolvePrint = resolve;
+ this.rejectPrint = reject;
+ });
+
+ // Mock PrintEventHandler with our Promises.
+ this.win.PrintEventHandler._showPrintDialog = () => showSystemDialogPromise;
+ this.win.PrintEventHandler._doPrint = (bc, settings) => {
+ this._printedSettings = settings;
+ return printPromise;
+ };
+ }
+
+ addMockPrinter(opts = {}) {
+ if (typeof opts == "string") {
+ opts = { name: opts };
+ }
+ let {
+ name = "Mock Printer",
+ paperList = [],
+ printerInfoPromise = Promise.resolve(),
+ } = opts;
+ let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+
+ let defaultSettings = PSSVC.newPrintSettings;
+ defaultSettings.printerName = name;
+ defaultSettings.toFileName = "";
+ defaultSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatNative;
+ defaultSettings.printToFile = false;
+
+ let printer = {
+ name,
+ supportsColor: Promise.resolve(true),
+ supportsMonochrome: Promise.resolve(true),
+ printerInfo: printerInfoPromise.then(() => ({
+ paperList,
+ defaultSettings,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPrinterInfo]),
+ })),
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPrinter]),
+ };
+
+ if (!this._mockPrinters) {
+ this._mockPrinters = [printer];
+ this.beforeInit(win => (win._mockPrinters = this._mockPrinters));
+ } else {
+ this._mockPrinters.push(printer);
+ }
+ return printer;
+ }
+
+ get _tabDialogBox() {
+ return this.sourceBrowser.ownerGlobal.gBrowser.getTabDialogBox(
+ this.sourceBrowser
+ );
+ }
+
+ get _tabDialogBoxManager() {
+ return this._tabDialogBox.getTabDialogManager();
+ }
+
+ get _dialogs() {
+ return this._tabDialogBox.getTabDialogManager()._dialogs;
+ }
+
+ get dialog() {
+ return this._dialogs.find(dlg =>
+ dlg._box.querySelector(".printSettingsBrowser")
+ );
+ }
+
+ get _printBrowser() {
+ return this.dialog._frame;
+ }
+
+ get doc() {
+ return this._printBrowser.contentDocument;
+ }
+
+ get win() {
+ return this._printBrowser.contentWindow;
+ }
+
+ get(id) {
+ return this.doc.getElementById(id);
+ }
+
+ get sourceURI() {
+ return this.win.PrintEventHandler.originalSourceCurrentURI;
+ }
+
+ async waitForPreview(changeFn) {
+ changeFn();
+ await BrowserTestUtils.waitForEvent(this.doc, "preview-updated");
+ }
+
+ async waitForSettingsEvent(changeFn) {
+ let changed = BrowserTestUtils.waitForEvent(this.doc, "print-settings");
+ await changeFn?.();
+ await BrowserTestUtils.waitForCondition(
+ () => !this.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
+ "Wait for all delayed tasks to execute"
+ );
+ await changed;
+ }
+
+ click(el, { scroll = true } = {}) {
+ if (scroll) {
+ el.scrollIntoView();
+ }
+ ok(BrowserTestUtils.is_visible(el), "Element must be visible to click");
+ EventUtils.synthesizeMouseAtCenter(el, {}, this.win);
+ }
+
+ text(el, text) {
+ this.click(el);
+ el.value = "";
+ EventUtils.sendString(text, this.win);
+ }
+
+ async openMoreSettings(options) {
+ let details = this.get("more-settings");
+ if (!details.open) {
+ this.click(details.firstElementChild, options);
+ }
+ await this.awaitAnimationFrame();
+ }
+
+ dispatchSettingsChange(settings) {
+ this.doc.dispatchEvent(
+ new CustomEvent("update-print-settings", {
+ detail: settings,
+ })
+ );
+ }
+
+ get settings() {
+ return this.win.PrintEventHandler.settings;
+ }
+
+ get viewSettings() {
+ return this.win.PrintEventHandler.viewSettings;
+ }
+
+ _assertMatches(a, b, msg) {
+ if (Array.isArray(a)) {
+ is(a.length, b.length, msg);
+ for (let i = 0; i < a.length; ++i) {
+ this._assertMatches(a[i], b[i], msg);
+ }
+ return;
+ }
+ is(a, b, msg);
+ }
+
+ assertSettingsMatch(expected) {
+ let { settings } = this;
+ for (let [setting, value] of Object.entries(expected)) {
+ this._assertMatches(settings[setting], value, `${setting} matches`);
+ }
+ }
+
+ assertPrintedWithSettings(expected) {
+ ok(this._printedSettings, "Printed settings have been recorded");
+ for (let [setting, value] of Object.entries(expected)) {
+ this._assertMatches(
+ this._printedSettings[setting],
+ value,
+ `${setting} matches printed setting`
+ );
+ }
+ }
+
+ async assertSettingsChanged(from, to, changeFn) {
+ is(
+ Object.keys(from).length,
+ Object.keys(to).length,
+ "Got the same number of settings to check"
+ );
+ ok(
+ Object.keys(from).every(s => s in to),
+ "Checking the same setting names"
+ );
+ this.assertSettingsMatch(from);
+ await changeFn();
+ this.assertSettingsMatch(to);
+ }
+
+ async assertSettingsNotChanged(settings, changeFn) {
+ await this.assertSettingsChanged(settings, settings, changeFn);
+ }
+
+ awaitAnimationFrame() {
+ return new Promise(resolve => this.win.requestAnimationFrame(resolve));
+ }
+
+ mockFilePickerCancel() {
+ if (!pickerMocked) {
+ pickerMocked = true;
+ MockFilePicker.init(window);
+ registerCleanupFunction(() => MockFilePicker.cleanup());
+ }
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ }
+
+ mockFilePicker(filename) {
+ if (!pickerMocked) {
+ pickerMocked = true;
+ MockFilePicker.init(window);
+ registerCleanupFunction(() => MockFilePicker.cleanup());
+ }
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(filename);
+ registerCleanupFunction(() => {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+ MockFilePicker.setFiles([file]);
+ return file;
+ }
+}
diff --git a/toolkit/components/printing/tests/longerArticle.html b/toolkit/components/printing/tests/longerArticle.html
new file mode 100644
index 0000000000..4109e55959
--- /dev/null
+++ b/toolkit/components/printing/tests/longerArticle.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ @media print {
+ #page-2 {
+ page-break-before: always;
+ }
+ #page-3 {
+ page-break-before: always;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <h1 id="page-1">Page 1</h1>
+ <h1 id="page-2">Page 2</h1>
+ <h1 id="page-3">Page 3</h1>
+ </body>
+</html>
diff --git a/toolkit/components/printing/tests/simplifyArticleSample.html b/toolkit/components/printing/tests/simplifyArticleSample.html
new file mode 100644
index 0000000000..70b172cf63
--- /dev/null
+++ b/toolkit/components/printing/tests/simplifyArticleSample.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu</p>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu</p>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu</p>
+</body>
+</html>
diff --git a/toolkit/components/printing/tests/simplifyNonArticleSample.html b/toolkit/components/printing/tests/simplifyNonArticleSample.html
new file mode 100644
index 0000000000..e216af3c1f
--- /dev/null
+++ b/toolkit/components/printing/tests/simplifyNonArticleSample.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non article title</title>
+<meta name="description" content="This is the non-article description." />
+</head>
+<body>
+</body>
+</html>