diff options
Diffstat (limited to 'toolkit/components/printing/content')
17 files changed, 6349 insertions, 0 deletions
diff --git a/toolkit/components/printing/content/landscape.svg b/toolkit/components/printing/content/landscape.svg new file mode 100644 index 0000000000..5e6bbd9e02 --- /dev/null +++ b/toolkit/components/printing/content/landscape.svg @@ -0,0 +1,8 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity"> + <path d="M11.5 2.5l4 4v6c0 0.6-0.4 1-1 1h-13c-0.6 0-1-0.4-1-1v-9c0-0.6 0.4-1 1-1h10z" fill-opacity=".1"/> + <path d="M15.5 6.5h-4v-4l4 4z" fill-opacity=".3"/> + <path d="M16 6.4c0-0.1-0.1-0.2-0.1-0.3l-4-4c-0.1 0-0.2-0.1-0.3-0.1H1.5C0.7 2 0 2.7 0 3.5v9C0 13.3 0.7 14 1.5 14h13c0.8 0 1.5-0.7 1.5-1.5V6.4zM14.3 6H12V3.7L14.3 6zm0.2 7h-13C1.2 13 1 12.8 1 12.5v-9C1 3.2 1.2 3 1.5 3H11v3.5C11 6.8 11.2 7 11.5 7H15v5.5c0 0.3-0.2 0.5-0.5 0.5z"/> +</svg> diff --git a/toolkit/components/printing/content/portrait.svg b/toolkit/components/printing/content/portrait.svg new file mode 100644 index 0000000000..f520820efb --- /dev/null +++ b/toolkit/components/printing/content/portrait.svg @@ -0,0 +1,8 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity"> + <path d="M13.5 4.5l-4-4h-6c-0.6 0-1 0.4-1 1v13c0 0.6 0.4 1 1 1h9c0.6 0 1-0.4 1-1v-10z" fill-opacity=".1"/> + <path d="M9.5 0.5v4h4l-4-4z" fill-opacity=".3"/> + <path d="M14 4.4c0-0.1-0.1-0.2-0.1-0.3l-4-4C9.8 0.1 9.7 0 9.6 0H3.5C2.7 0 2 0.7 2 1.5v13C2 15.3 2.7 16 3.5 16h9c0.8 0 1.5-0.7 1.5-1.5V4.4zM12.3 4H10V1.7L12.3 4zm0.2 11h-9C3.2 15 3 14.8 3 14.5v-13C3 1.2 3.2 1 3.5 1H9v3.5C9 4.8 9.2 5 9.5 5H13v9.5c0 0.3-0.2 0.5-0.5 0.5z"/> +</svg> diff --git a/toolkit/components/printing/content/print.css b/toolkit/components/printing/content/print.css new file mode 100644 index 0000000000..0c2b0f10e3 --- /dev/null +++ b/toolkit/components/printing/content/print.css @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +html, body { + width: 250px; + height: 100vh; +} + +body { + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow: hidden; + background: var(--in-content-box-background); +} + +body[loading] #print { + visibility: hidden; +} + +*[hidden] { + display: none !important; +} + +.section-block { + margin: 16px; +} + +.section-block .block-label { + display: block; + margin-bottom: 8px; +} + +.row { + display: flex; + flex-direction: column; + width: 100%; + min-height: 1.8em; + margin-block: 2px; +} + +.row > input, +.row > select, +.col > input { + margin-inline: 0 0; + max-width: 100%; + display: inline-block; +} + +.row.cols-2 { + flex-direction: row; + align-items: center; +} + +.cols-2 > .col:nth-child(1) { + flex: 0 0 2em; + text-align: center; +} + +.header-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex: 0 1 auto; + border-bottom: 1px solid var(--in-content-border-color); + padding: 8px 18px; +} +.header-container > h2 { + margin: 0; + font-size: 17px; + line-height: 1; +} + +#sheet-count { + font-size: 11px; + padding: 3px 8px; + margin: 0; +} + +#sheet-count[loading], +body[invalid] #sheet-count { + visibility: hidden; +} + +form#print { + display: flex; + flex: 1 1 auto; + flex-direction: column; + justify-content: flex-start; + overflow: hidden; + font: menu; +} + +select { + min-height: auto; + margin: 0; + padding: 0; +} + +select:not([size], [multiple])[iconic] { + padding-inline-start: 28px; +} + +#printer-picker { + background-size: auto 12px, 16px; + background-image: url("chrome://global/skin/icons/arrow-dropdown-12.svg"), url("chrome://global/skin/icons/print.svg"); + background-position: right 3px center, left 8px center; + width: 100%; +} + +#printer-picker:dir(rtl) { + background-position-x: left 3px, right 8px; +} + +#printer-picker[output="pdf"] { + background-image: url("chrome://global/skin/icons/arrow-dropdown-12.svg"), url("chrome://global/content/portrait.svg"); +} + +input[type="checkbox"] { + margin-inline-end: 8px; +} + +input[type="radio"] { + appearance: none; + width: 20px; + height: 20px; + border: 1px solid var(--in-content-box-border-color); + border-radius: 50%; + margin: 0; + margin-inline-end: 8px; + background-color: var(--in-content-box-background); +} + +input[type="radio"]:checked { + appearance: none; + background-image: url("chrome://global/skin/in-content/radio.svg"); + -moz-context-properties: fill; + fill: #3485ff; +} + +input[type="radio"]:enabled:-moz-focusring, +input[type="radio"]:enabled:hover { + border-color: var(--in-content-border-focus); +} + +input[type="radio"]:enabled:-moz-focusring { + outline: 2px solid var(--in-content-border-active); + /* offset outline to align with 1px border-width set for buttons/menulists above. */ + outline-offset: -1px; + /* Make outline-radius slightly bigger than the border-radius set above, + * to make the thicker outline corners look smooth */ + -moz-outline-radius: 50%; + box-shadow: 0 0 0 4px var(--in-content-border-active-shadow); +} + +input[type="radio"]:disabled { + opacity: 0.5; +} + +input[type="number"], +input[type="text"] { + padding: 2px; + padding-inline-start: 4px; + outline: none !important; +} + +.cols-2 > input { + flex: none; +} + +.toggle-group-label { + padding: 4px 8px; +} + +.body-container { + flex: 1 1 auto; + overflow: auto; +} + +#more-settings { + border-top: 1px solid var(--in-content-border-color); +} + +.twisty > summary { + list-style: none; + display: flex; + cursor: pointer; + align-items: center; +} + +#more-settings > summary > .twisty { + background-image: url("chrome://global/skin/icons/twisty-expanded.svg"); + background-repeat: no-repeat; + background-position: center; + -moz-context-properties: fill; + fill: currentColor; + width: 12px; + height: 12px; + scale: 1 1; +} + +#more-settings > summary > .label { + flex-grow: 1; +} + +#more-settings[open] > summary > .twisty { + /* flip arrow to point up for the open state */ + scale: 1 -1; +} + +#open-dialog-link { + display: flex; + justify-content: space-between; + align-items: center; +} + +#open-dialog-link::after { + display: block; + content: url(chrome://global/skin/icons/open-in-new.svg); + -moz-context-properties: fill; + fill: currentColor; + width: 12px; + height: 12px; +} + +#open-dialog-link:dir(rtl)::after { + scale: -1 1; +} + +.footer-container { + border-top: 1px solid var(--in-content-border-color); + flex: 0 1 auto; +} + +#print-progress { + background-image: url("chrome://global/skin/icons/loading.png"); + background-repeat: no-repeat; + background-size: 16px; + background-position: left center; + padding-inline-start: 20px; +} + +@media (min-resolution: 1.1dppx) { + #print-progress { + background-image: url("chrome://global/skin/icons/loading@2x.png"); + } +} + +#print-progress:dir(rtl) { + background-position-x: right; +} + +#button-container { + display: flex; + justify-content: center; + gap: 8px; +} + +#button-container > button { + flex: 1 1 auto; + margin: 0; +} + +#custom-range { + margin-top: 4px; +} + +.vertical-margins, +.horizontal-margins { + display: flex; + gap: 20px; + margin-block: 5px; +} + +.margin-input { + width: 6em !important; +} + +.margin-descriptor { + text-align: center; + display: block; + margin-top: 2px; +} + +.toggle-group #landscape + .toggle-group-label::before { + content: url("chrome://global/content/landscape.svg"); +} +.toggle-group #portrait + .toggle-group-label::before { + content: url("chrome://global/content/portrait.svg"); +} + +select:invalid, +input[type="text"]:invalid, +input[type="number"]:invalid { + border: 1px solid #D70022; + box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3); +} + +.error-message { + font-size: 12px; + color: #D70022; + margin-top: 4px; +} + +#percent-scale { + margin-inline-start: 4px; +} + +input[type="number"].photon-number { + padding: 0; + padding-inline-start: 4px; + margin: 0; + height: 20px; + width: 4em; +} + +input[type="number"].photon-number::-moz-number-spin-box { + height: 100%; + max-height: 100%; + border-inline-start: 1px solid var(--in-content-box-border-color); + width: 1em; +} + +input[type="number"].photon-number:hover::-moz-number-spin-box { + border-color: var(--in-content-border-hover); +} + +input[type="number"].photon-number::-moz-number-spin-up, +input[type="number"].photon-number::-moz-number-spin-down { + border: 0; + border-radius: 0; + background-color: var(--in-content-button-background); + background-image: url("chrome://global/skin/icons/arrow-dropdown-16.svg"); + background-size: 8px; + background-position: center; + -moz-context-properties: fill; + fill: currentColor; +} + +input[type="number"].photon-number::-moz-number-spin-up { + scale: 1 -1; +} + +input[type="number"].photon-number::-moz-number-spin-up:hover, +input[type="number"].photon-number::-moz-number-spin-down:hover { + background-color: var(--in-content-button-background-hover); +} diff --git a/toolkit/components/printing/content/print.html b/toolkit/components/printing/content/print.html new file mode 100644 index 0000000000..7394069efe --- /dev/null +++ b/toolkit/components/printing/content/print.html @@ -0,0 +1,228 @@ +<!doctype html> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> + <head> + <meta charset="utf-8"> + <title data-l10n-id="printui-title"></title> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:;img-src data:; object-src 'none'"> + + <link rel="localization" href="toolkit/printing/printUI.ftl"> + + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <link rel="stylesheet" href="chrome://global/content/toggle-group.css"> + <link rel="stylesheet" href="chrome://global/content/print.css"> + <script defer src="chrome://global/content/print.js"></script> + </head> + + <body loading rendering> + <template id="page-range-template"> + <select id="range-picker" name="page-range-type" data-l10n-id="printui-page-range-picker" is="setting-select"> + <option value="all" selected data-l10n-id="printui-page-range-all"></option> + <option value="custom" data-l10n-id="printui-page-range-custom"></option> + </select> + <input id="custom-range" type="text" disabled hidden data-l10n-id="printui-page-custom-range-input" aria-errormessage="error-invalid-range error-invalid-start-range-overflow"> + <p id="error-invalid-range" hidden data-l10n-id="printui-error-invalid-range" class="error-message" role="alert" data-l10n-args='{ "numPages": 1 }'></p> + <p id="error-invalid-start-range-overflow" hidden data-l10n-id="printui-error-invalid-start-overflow" class="error-message" role="alert"></p> + </template> + + <template id="orientation-template"> + <input type="radio" name="orientation" id="portrait" value="0" checked class="toggle-group-input"> + <label for="portrait" data-l10n-id="printui-portrait" class="toggle-group-label toggle-group-label-iconic"></label> + <input type="radio" name="orientation" id="landscape" value="1" class="toggle-group-input"> + <label for="landscape" data-l10n-id="printui-landscape" class="toggle-group-label toggle-group-label-iconic"></label> + </template> + + <template id="twisty-summary-template"> + <span class="label"></span> + <span class="twisty"></span> + </template> + + <template id="scale-template"> + <div role="radiogroup" aria-labelledby="scale-label"> + <div class="row cols-2"> + <input type="radio" name="scale-choice" id="fit-choice" value="fit" checked> + <label for="fit-choice" data-l10n-id="printui-scale-fit-to-page-width" class="col"></label> + </div> + <div class="row cols-2"> + <input type="radio" name="scale-choice" id="percent-scale-choice"> + <span class="col"> + <label id="percent-scale-label" for="percent-scale-choice" data-l10n-id="printui-scale-pcent"></label> + <!-- Note that here and elsewhere, we're setting aria-errormessage + attributes to a list of all possible errors. The a11y APIs + will filter this down to visible items only. --> + <input + id="percent-scale" class="photon-number" is="setting-number" + min="10" max="200" step="1" size="6" + aria-labelledby="percent-scale-label" + aria-errormessage="error-invalid-scale" + disabled required> + </span> + </div> + <p id="error-invalid-scale" hidden data-l10n-id="printui-error-invalid-scale" class="error-message" role="alert"></p> + </div> + </template> + + <template id="margins-template"> + <label for="margins-picker" class="block-label" data-l10n-id="printui-margins"></label> + <select is="margins-select" id="margins-picker" name="margin-type" class="row" data-setting-name="margins"> + <option value="default" data-l10n-id="printui-margins-default"></option> + <option value="minimum" data-l10n-id="printui-margins-min"></option> + <option value="none" data-l10n-id="printui-margins-none"></option> + <option value="custom" data-l10n-id="printui-margins-custom-inches"></option> + </select> + <div id="custom-margins" class="margin-group" role="group" hidden> + <div class="vertical-margins"> + <div class="margin-pair"> + <input is="setting-number" + id="custom-margin-top" class="margin-input photon-number" + aria-describedby="margins-custom-margin-top-desc" + min="0" step="0.01" required> + <label for="custom-margin-top" class="margin-descriptor" data-l10n-id="printui-margins-custom-top"></label> + <label hidden id="margins-custom-margin-top-desc" data-l10n-id="printui-margins-custom-top-inches"></label> + </div> + <div class="margin-pair"> + <input is="setting-number" + id="custom-margin-bottom" class="margin-input photon-number" + aria-describedby="margins-custom-margin-bottom-desc" + min="0" step="0.01" required> + <label for="custom-margin-bottom" class="margin-descriptor" data-l10n-id="printui-margins-custom-bottom"></label> + <label hidden id="margins-custom-margin-bottom-desc" data-l10n-id="printui-margins-custom-bottom-inches"></label> + </div> + </div> + <div class="horizontal-margins"> + <div class="margin-pair"> + <input is="setting-number" + id="custom-margin-left" class="margin-input photon-number" + aria-describedby="margins-custom-margin-left-desc" + min="0" step="0.01" required> + <label for="custom-margin-left" class="margin-descriptor" data-l10n-id="printui-margins-custom-left"></label> + <label hidden id="margins-custom-margin-left-desc" data-l10n-id="printui-margins-custom-left-inches"></label> + </div> + <div class="margin-pair"> + <input is="setting-number" + id="custom-margin-right" class="margin-input photon-number" + aria-describedby="margins-custom-margin-right-desc" + min="0" step="0.01" required> + <label for="custom-margin-right" class="margin-descriptor" data-l10n-id="printui-margins-custom-right"></label> + <label hidden id="margins-custom-margin-right-desc" data-l10n-id="printui-margins-custom-right-inches"></label> + </div> + </div> + <p id="error-invalid-margin" hidden data-l10n-id="printui-error-invalid-margin" class="error-message" role="alert"></p> + </div> + </template> + + <header class="header-container" role="none"> + <h2 data-l10n-id="printui-title"></h2> + <div aria-live="off"> + <p id="sheet-count" is="page-count" data-l10n-id="printui-sheets-count" data-l10n-args='{ "sheetCount": 0 }' loading></p> + </div> + </header> + + <form id="print" is="print-form" aria-labelledby="page-header"> + <section class="body-container"> + <section id="destination" class="section-block"> + <label for="printer-picker" class="block-label" data-l10n-id="printui-destination-label"></label> + <div class="printer-picker-wrapper"> + <select is="destination-picker" id="printer-picker" data-setting-name="printerName" iconic></select> + </div> + </section> + <section id="settings"> + <section id="copies" class="section-block"> + <label for="copies-count" class="block-label" data-l10n-id="printui-copies-label"></label> + <input id="copies-count" is="setting-number" data-setting-name="numCopies" min="1" max="10000" class="copy-count-input photon-number" required> + </section> + + <section id="orientation" class="section-block"> + <label id="orientation-label" class="block-label" data-l10n-id="printui-orientation"></label> + <div is="orientation-input" class="toggle-group" role="radiogroup" aria-labelledby="orientation-label"></div> + </section> + + <section id="pages" class="section-block"> + <label for="page-range-input" class="block-label" data-l10n-id="printui-page-range-label"></label> + <div id="page-range-input" is="page-range-input" class="page-range-input row"></div> + </section> + + <section id="color-mode" class="section-block"> + <label for="color-mode-picker" class="block-label" data-l10n-id="printui-color-mode-label"></label> + <select is="color-mode-select" id="color-mode-picker" class="row" data-setting-name="printInColor"> + <option value="color" selected data-l10n-id="printui-color-mode-color"></option> + <option value="bw" data-l10n-id="printui-color-mode-bw"></option> + </select> + </section> + + <details id="more-settings" class="twisty"> + <summary class="block-label section-block" is="twisty-summary" + data-open-l10n-id="printui-less-settings" + data-closed-l10n-id="printui-more-settings"></summary> + + <section id="paper-size" class="section-block"> + <label for="paper-size-picker" class="block-label" data-l10n-id="printui-paper-size-label"></label> + <select is="paper-size-select" id="paper-size-picker" class="row" data-setting-name="paperId"> + </select> + </section> + + <section id="scale" class="section-block"> + <label id="scale-label" class="block-label" data-l10n-id="printui-scale"></label> + <scale-input></scale-input> + </section> + + <section id="pages-per-sheet" class="section-block" hidden> + <label id="pages-per-sheet-label" for="pages-per-sheet-picker" class="block-label" data-l10n-id="printui-pages-per-sheet"></label> + <select is="setting-select" id="pages-per-sheet-picker" class="row" data-setting-name="numPagesPerSheet"> + <option value="1">1</option> + <option value="2">2</option> + <option value="4">4</option> + <option value="6">6</option> + <option value="9">9</option> + <option value="16">16</option> + </select> + </section> + + <section id="margins" class="section-block"> + <div id="margins-select" is="margins-select" class="margins-select row"></div> + </section> + + <section id="two-sided-printing" class="section-block"> + <label class="block-label" data-l10n-id="printui-two-sided-printing"></label> + <div class="row cols-2"> + <input is="setting-checkbox" id="duplex-enabled" data-setting-name="printDuplex"> + <label for="duplex-enabled" data-l10n-id="printui-duplex-checkbox"></label> + </div> + </section> + + <section id="more-settings-options" class="section-block"> + <label class="block-label" data-l10n-id="printui-options"></label> + <div id="headers-footers" class="row cols-2"> + <input is="setting-checkbox" id="headers-footers-enabled" data-setting-name="printFootersHeaders"> + <label for="headers-footers-enabled" data-l10n-id="printui-headers-footers-checkbox"></label> + </div> + <div id="backgrounds" class="row cols-2"> + <input is="setting-checkbox" id="backgrounds-enabled" data-setting-name="printBackgrounds"> + <label for="backgrounds-enabled" data-l10n-id="printui-backgrounds-checkbox"></label> + </div> + <div id="print-selection-container" class="row cols-2" hidden> + <input is="setting-checkbox" id="print-selection-enabled" data-setting-name="printSelectionOnly"> + <label for="print-selection-enabled" data-l10n-id="printui-selection-checkbox"></label> + </div> + </section> + + </details> + </section> + + <section id="system-print" class="section-block"> + <a href="#" id="open-dialog-link" data-l10n-id="printui-system-dialog-link"></a> + </section> + </section> + + <footer class="footer-container" id="print-footer" role="none"> + <p id="print-progress" class="section-block" data-l10n-id="printui-print-progress-indicator" hidden></p> + <section id="button-container" class="section-block"> + <button id="print-button" class="primary" showfocus name="print" data-l10n-id="printui-primary-button" is="print-button" type="submit"></button> + <button id="cancel-button" name="cancel" data-l10n-id="printui-cancel-button" data-close-l10n-id="printui-close-button" data-cancel-l10n-id="printui-cancel-button" type="button" is="cancel-button"></button> + </section> + </footer> + </form> + </body> +</html> diff --git a/toolkit/components/printing/content/print.js b/toolkit/components/printing/content/print.js new file mode 100644 index 0000000000..826893a0ec --- /dev/null +++ b/toolkit/components/printing/content/print.js @@ -0,0 +1,2562 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { + gBrowser, + PrintUtils, + Services, + AppConstants, +} = window.docShell.chromeEventHandler.ownerGlobal; + +ChromeUtils.defineModuleGetter( + this, + "DownloadPaths", + "resource://gre/modules/DownloadPaths.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "DeferredTask", + "resource://gre/modules/DeferredTask.jsm" +); + +const PDF_JS_URI = "resource://pdf.js/web/viewer.html"; +const INPUT_DELAY_MS = Cu.isInAutomation ? 100 : 500; +const MM_PER_POINT = 25.4 / 72; +const INCHES_PER_POINT = 1 / 72; +const ourBrowser = window.docShell.chromeEventHandler; +const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService +); + +var logger = (function() { + const getMaxLogLevel = () => + Services.prefs.getBoolPref("print.debug", false) ? "all" : "warn"; + + let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm"); + // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref. + let _logger = new ConsoleAPI({ + prefix: "printUI", + maxLogLevel: getMaxLogLevel(), + }); + + function onPrefChange() { + if (_logger) { + _logger.maxLogLevel = getMaxLogLevel(); + } + } + // Watch for pref changes and the maxLogLevel for the logger + Services.prefs.addObserver("print.debug", onPrefChange); + window.addEventListener("unload", () => { + Services.prefs.removeObserver("print.debug", onPrefChange); + }); + return _logger; +})(); + +function serializeSettings(settings, logPrefix) { + let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception? + let types = new Set(["string", "boolean", "number", "undefined"]); + let nameValues = {}; + for (let key in settings) { + try { + if (!re.test(key) && types.has(typeof settings[key])) { + nameValues[key] = settings[key]; + } + } catch (e) { + logger.warn("Exception accessing setting: ", key, e); + } + } + return nameValues; +} + +let printPending = false; +let deferredTasks = []; +function createDeferredTask(fn, timeout) { + let task = new DeferredTask(fn, timeout); + deferredTasks.push(task); + return task; +} + +function cancelDeferredTasks() { + for (let task of deferredTasks) { + task.disarm(); + } + PrintEventHandler._updatePrintPreviewTask?.disarm(); + deferredTasks = []; +} + +document.addEventListener( + "DOMContentLoaded", + e => { + window._initialized = PrintEventHandler.init(); + ourBrowser.setAttribute("flex", "0"); + ourBrowser.classList.add("printSettingsBrowser"); + ourBrowser.closest(".dialogBox")?.classList.add("printDialogBox"); + }, + { once: true } +); + +window.addEventListener("dialogclosing", () => { + PrintEventHandler.unload(); + cancelDeferredTasks(); +}); + +window.addEventListener( + "unload", + e => { + document.textContent = ""; + }, + { once: true } +); + +var PrintEventHandler = { + settings: null, + defaultSettings: null, + allPaperSizes: {}, + previewIsEmpty: false, + _delayedChanges: {}, + _hasRenderedSelectionPreview: false, + _hasRenderedPrimaryPreview: false, + _userChangedSettings: {}, + settingFlags: { + margins: Ci.nsIPrintSettings.kInitSaveMargins, + customMargins: Ci.nsIPrintSettings.kInitSaveMargins, + orientation: Ci.nsIPrintSettings.kInitSaveOrientation, + paperId: + Ci.nsIPrintSettings.kInitSavePaperSize | + Ci.nsIPrintSettings.kInitSaveUnwriteableMargins, + printInColor: Ci.nsIPrintSettings.kInitSaveInColor, + scaling: Ci.nsIPrintSettings.kInitSaveScaling, + shrinkToFit: Ci.nsIPrintSettings.kInitSaveShrinkToFit, + printDuplex: Ci.nsIPrintSettings.kInitSaveDuplex, + printFootersHeaders: + Ci.nsIPrintSettings.kInitSaveHeaderLeft | + Ci.nsIPrintSettings.kInitSaveHeaderCenter | + Ci.nsIPrintSettings.kInitSaveHeaderRight | + Ci.nsIPrintSettings.kInitSaveFooterLeft | + Ci.nsIPrintSettings.kInitSaveFooterCenter | + Ci.nsIPrintSettings.kInitSaveFooterRight, + printBackgrounds: + Ci.nsIPrintSettings.kInitSaveBGColors | + Ci.nsIPrintSettings.kInitSaveBGImages, + }, + originalSourceContentTitle: null, + originalSourceCurrentURI: null, + previewBrowser: null, + selectionPreviewBrowser: null, + currentPreviewBrowser: null, + + // These settings do not have an associated pref value or flag, but + // changing them requires us to update the print preview. + _nonFlaggedUpdatePreviewSettings: new Set([ + "pageRanges", + "numPagesPerSheet", + "printSelectionOnly", + ]), + _noPreviewUpdateSettings: new Set(["numCopies", "printDuplex"]), + + async init() { + Services.telemetry.scalarAdd("printing.preview_opened_tm", 1); + + // Do not keep a reference to source browser, it may mutate after printing + // is initiated and the print preview clone must be a snapshot from the + // time that the print was started. + let sourceBrowsingContext = this.getSourceBrowsingContext(); + ({ + previewBrowser: this.previewBrowser, + selectionPreviewBrowser: this.selectionPreviewBrowser, + } = PrintUtils.createPreviewBrowsers(sourceBrowsingContext, ourBrowser)); + + let args = window.arguments[0]; + this.printSelectionOnly = args.getProperty("printSelectionOnly"); + this.hasSelection = + args.getProperty("hasSelection") && this.selectionPreviewBrowser; + this.printFrameOnly = args.getProperty("printFrameOnly"); + // Get the temporary browser that will previously have been created for the + // platform code to generate the static clone printing doc into if this + // print is for a window.print() call. In that case we steal the browser's + // docshell to get the static clone, then discard it. + let existingBrowser = args.getProperty("previewBrowser"); + if (existingBrowser) { + sourceBrowsingContext = existingBrowser.browsingContext; + this.previewBrowser.swapDocShells(existingBrowser); + existingBrowser.remove(); + } + document.querySelector("#print-selection-container").hidden = !this + .hasSelection; + + let sourcePrincipal = + sourceBrowsingContext.currentWindowGlobal.documentPrincipal; + let sourceIsPdf = + !sourcePrincipal.isNullPrincipal && sourcePrincipal.spec == PDF_JS_URI; + this.originalSourceContentTitle = + sourceBrowsingContext.currentWindowContext.documentTitle; + this.originalSourceCurrentURI = + sourceBrowsingContext.currentWindowContext.documentURI.spec; + + this.sourceWindowId = this.printFrameOnly + ? sourceBrowsingContext.currentWindowGlobal.outerWindowId + : sourceBrowsingContext.top.embedderElement.browsingContext + .currentWindowGlobal.outerWindowId; + this.selectionWindowId = + sourceBrowsingContext.currentWindowGlobal.outerWindowId; + + // We don't need the sourceBrowsingContext anymore, get rid of it. + sourceBrowsingContext = undefined; + + this.printProgressIndicator = document.getElementById("print-progress"); + this.printForm = document.getElementById("print"); + if (sourceIsPdf) { + this.printForm.removeNonPdfSettings(); + } + + // Let the dialog appear before doing any potential main thread work. + await ourBrowser._dialogReady; + + // First check the available destinations to ensure we get settings for an + // accessible printer. + let destinations, + defaultSystemPrinter, + fallbackPaperList, + selectedPrinter, + printersByName; + try { + ({ + destinations, + defaultSystemPrinter, + fallbackPaperList, + selectedPrinter, + printersByName, + } = await this.getPrintDestinations()); + } catch (e) { + this.reportPrintingError("PRINT_DESTINATIONS"); + throw e; + } + PrintSettingsViewProxy.availablePrinters = printersByName; + PrintSettingsViewProxy.fallbackPaperList = fallbackPaperList; + PrintSettingsViewProxy.defaultSystemPrinter = defaultSystemPrinter; + + logger.debug("availablePrinters: ", Object.keys(printersByName)); + logger.debug("defaultSystemPrinter: ", defaultSystemPrinter); + + document.addEventListener("print", async () => { + let cancelButton = document.getElementById("cancel-button"); + document.l10n.setAttributes( + cancelButton, + cancelButton.dataset.closeL10nId + ); + let didPrint = await this.print(); + if (!didPrint) { + // Re-enable elements of the form if the user cancels saving or + // if a deferred task rendered the page invalid. + this.printForm.enable(); + } + // Reset the cancel button regardless of the outcome. + document.l10n.setAttributes( + cancelButton, + cancelButton.dataset.cancelL10nId + ); + }); + this._createDelayedSettingsChangeTask(); + document.addEventListener("update-print-settings", e => { + this.handleSettingsChange(e.detail); + }); + document.addEventListener("cancel-print-settings", e => { + this._delayedSettingsChangeTask.disarm(); + for (let setting of Object.keys(e.detail)) { + delete this._delayedChanges[setting]; + } + }); + document.addEventListener("cancel-print", () => this.cancelPrint()); + document.addEventListener("open-system-dialog", async () => { + // This file in only used if pref print.always_print_silent is false, so + // no need to check that here. + + // Hide the dialog box before opening system dialog + // We cannot close the window yet because the browsing context for the + // print preview browser is needed to print the page. + let sourceBrowser = this.getSourceBrowsingContext().top.embedderElement; + let dialogBoxManager = gBrowser + .getTabDialogBox(sourceBrowser) + .getTabDialogManager(); + dialogBoxManager.hideDialog(sourceBrowser); + + // Use our settings to prepopulate the system dialog. + // The system print dialog won't recognize our internal save-to-pdf + // pseudo-printer. We need to pass it a settings object from any + // system recognized printer. + let settings = + this.settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? PrintUtils.getPrintSettings(this.viewSettings.defaultSystemPrinter) + : this.settings.clone(); + settings.showPrintProgress = false; + // We set the title so that if the user chooses save-to-PDF from the + // system dialog the title will be used to generate the prepopulated + // filename in the file picker. + settings.title = this.previewBrowser.browsingContext.embedderElement.contentTitle; + const PRINTPROMPTSVC = Cc[ + "@mozilla.org/embedcomp/printingprompt-service;1" + ].getService(Ci.nsIPrintingPromptService); + try { + Services.telemetry.scalarAdd( + "printing.dialog_opened_via_preview_tm", + 1 + ); + await this._showPrintDialog(PRINTPROMPTSVC, window, settings); + } catch (e) { + if (e.result == Cr.NS_ERROR_ABORT) { + Services.telemetry.scalarAdd( + "printing.dialog_via_preview_cancelled_tm", + 1 + ); + window.close(); + return; // user cancelled + } + throw e; + } + await this.print(settings); + }); + + let settingsToChange = await this.refreshSettings(selectedPrinter.value); + await this.updateSettings(settingsToChange, true); + + let initialPreviewDone = this._updatePrintPreview(); + + // Use a DeferredTask for updating the preview. This will ensure that we + // only have one update running at a time. + this._createUpdatePrintPreviewTask(initialPreviewDone); + + document.dispatchEvent( + new CustomEvent("available-destinations", { + detail: destinations, + }) + ); + + document.dispatchEvent( + new CustomEvent("print-settings", { + detail: this.viewSettings, + }) + ); + + await document.l10n.translateElements([this.previewBrowser]); + if (this.selectionPreviewBrowser) { + await document.l10n.translateElements([this.selectionPreviewBrowser]); + } + + document.body.removeAttribute("loading"); + + await new Promise(resolve => window.requestAnimationFrame(resolve)); + + // Now that we're showing the form, select the destination select. + window.focus(); + let fm = Services.focus; + fm.setFocus(document.getElementById("printer-picker"), fm.FLAG_SHOWRING); + + await initialPreviewDone; + }, + + unload() { + this.previewBrowser.frameLoader.exitPrintPreview(); + if (this.selectionPreviewBrowser) { + this.selectionPreviewBrowser.frameLoader.exitPrintPreview(); + } + }, + + async print(systemDialogSettings) { + // Disable the form when a print is in progress + this.printForm.disable(); + + if (Object.keys(this._delayedChanges).length) { + // Make sure any pending changes get saved. + let task = this._delayedSettingsChangeTask; + this._createDelayedSettingsChangeTask(); + await task.finalize(); + } + + if (this.settings.pageRanges.length) { + // Finish any running previews to verify the range is still valid. + let task = this._updatePrintPreviewTask; + this._createUpdatePrintPreviewTask(); + await task.finalize(); + } + + if (!this.printForm.checkValidity() || this.previewIsEmpty) { + return false; + } + + let settings = systemDialogSettings || this.settings; + + if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { + try { + settings.toFileName = await pickFileName( + this.originalSourceContentTitle, + this.originalSourceCurrentURI + ); + } catch (e) { + return false; + } + } + + await window._initialized; + + // This seems like it should be handled automatically but it isn't. + Services.prefs.setStringPref("print_printer", settings.printerName); + + try { + // We'll provide our own progress indicator. + this.settings.showPrintProgress = false; + let l10nId = + settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? "printui-print-progress-indicator-saving" + : "printui-print-progress-indicator"; + document.l10n.setAttributes(this.printProgressIndicator, l10nId); + this.printProgressIndicator.hidden = false; + + let bc = this.currentPreviewBrowser.browsingContext; + await this._doPrint(bc, settings); + } catch (e) { + Cu.reportError(e); + } + + window.close(); + return true; + }, + + cancelPrint() { + Services.telemetry.scalarAdd("printing.preview_cancelled_tm", 1); + window.close(); + }, + + async refreshSettings(printerName) { + this.currentPrinterName = printerName; + let currentPrinter; + try { + currentPrinter = await PrintSettingsViewProxy.resolvePropertiesForPrinter( + printerName + ); + } catch (e) { + this.reportPrintingError("PRINTER_PROPERTIES"); + throw e; + } + if (this.currentPrinterName != printerName) { + // Refresh settings could take a while, if the destination has changed + // then we don't want to update the settings after all. + return {}; + } + + this.settings = currentPrinter.settings; + this.defaultSettings = currentPrinter.defaultSettings; + + this.settings.printSelectionOnly = this.printSelectionOnly; + + logger.debug("currentPrinter name: ", printerName); + logger.debug("settings:", serializeSettings(this.settings)); + + // Some settings are only used by the UI + // assigning new values should update the underlying settings + this.viewSettings = new Proxy(this.settings, PrintSettingsViewProxy); + return this.getSettingsToUpdate(); + }, + + getSettingsToUpdate() { + // Get the previously-changed settings we want to try to use on this printer + let settingsToUpdate = Object.assign({}, this._userChangedSettings); + + // Ensure the color option is correct, if either of the supportsX flags are + // false then the user cannot change the value through the UI. + if (!this.viewSettings.supportsColor) { + settingsToUpdate.printInColor = false; + } else if (!this.viewSettings.supportsMonochrome) { + settingsToUpdate.printInColor = true; + } + + if ( + settingsToUpdate.printInColor != this._userChangedSettings.printInColor + ) { + delete this._userChangedSettings.printInColor; + } + + // See if the paperId needs to change. + let paperId = settingsToUpdate.paperId || this.viewSettings.paperId; + + logger.debug("Using paperId: ", paperId); + logger.debug( + "Available paper sizes: ", + PrintSettingsViewProxy.availablePaperSizes + ); + let matchedPaper = + paperId && PrintSettingsViewProxy.availablePaperSizes[paperId]; + if (!matchedPaper) { + let paperWidth, paperHeight, paperSizeUnit; + if (settingsToUpdate.paperId) { + // The user changed paperId in this instance and session, + // We should have details on the paper size from the previous printer + paperId = settingsToUpdate.paperId; + let cachedPaperWrapper = this.allPaperSizes[paperId]; + // for the purposes of finding a best-size match, we'll use mm + paperWidth = cachedPaperWrapper.paper.width * MM_PER_POINT; + paperHeight = cachedPaperWrapper.paper.height * MM_PER_POINT; + paperSizeUnit = PrintEventHandler.settings.kPaperSizeMillimeters; + } else { + paperId = this.viewSettings.paperId; + paperWidth = this.viewSettings.paperWidth; + paperHeight = this.viewSettings.paperHeight; + paperSizeUnit = this.viewSettings.paperSizeUnit; + } + matchedPaper = PrintSettingsViewProxy.getBestPaperMatch( + paperWidth, + paperHeight, + paperSizeUnit + ); + } + if (!matchedPaper) { + // We didn't find a good match. Take the first paper size + matchedPaper = Object.values( + PrintSettingsViewProxy.availablePaperSizes + )[0]; + delete this._userChangedSettings.paperId; + } + if (matchedPaper.id !== paperId) { + // The exact paper id doesn't exist for this printer + logger.log( + `Requested paperId: "${paperId}" missing on this printer, using: ${matchedPaper.id} instead` + ); + delete this._userChangedSettings.paperId; + } + // Always write paper details back to settings + settingsToUpdate.paperId = matchedPaper.id; + + return settingsToUpdate; + }, + + _createDelayedSettingsChangeTask() { + this._delayedSettingsChangeTask = createDeferredTask(async () => { + if (Object.keys(this._delayedChanges).length) { + let changes = this._delayedChanges; + this._delayedChanges = {}; + await this.onUserSettingsChange(changes); + } + }, INPUT_DELAY_MS); + }, + + _createUpdatePrintPreviewTask(initialPreviewDone = null) { + this._updatePrintPreviewTask = new DeferredTask(async () => { + await initialPreviewDone; + await this._updatePrintPreview(); + document.dispatchEvent(new CustomEvent("preview-updated")); + }, 0); + }, + + _scheduleDelayedSettingsChange(changes) { + Object.assign(this._delayedChanges, changes); + this._delayedSettingsChangeTask.disarm(); + this._delayedSettingsChangeTask.arm(); + }, + + handleSettingsChange(changedSettings = {}) { + let delayedChanges = {}; + let instantChanges = {}; + for (let [setting, value] of Object.entries(changedSettings)) { + switch (setting) { + case "pageRanges": + case "scaling": + delayedChanges[setting] = value; + break; + case "customMargins": + delete this._delayedChanges.margins; + changedSettings.margins == "custom" + ? (delayedChanges[setting] = value) + : (instantChanges[setting] = value); + break; + default: + instantChanges[setting] = value; + break; + } + } + if (Object.keys(delayedChanges).length) { + this._scheduleDelayedSettingsChange(delayedChanges); + } + if (Object.keys(instantChanges).length) { + this.onUserSettingsChange(instantChanges); + } + }, + + async onUserSettingsChange(changedSettings = {}) { + let previewableChange = false; + for (let [setting, value] of Object.entries(changedSettings)) { + Services.telemetry.keyedScalarAdd( + "printing.settings_changed", + setting, + 1 + ); + // Update the list of user-changed settings, which we attempt to maintain + // across printer changes. + this._userChangedSettings[setting] = value; + if (!this._noPreviewUpdateSettings.has(setting)) { + previewableChange = true; + } + } + if (changedSettings.printerName) { + logger.debug( + "onUserSettingsChange, changing to printerName:", + changedSettings.printerName + ); + this.printForm.printerChanging = true; + this.printForm.disable(el => el.id != "printer-picker"); + let { printerName } = changedSettings; + // Treat a printerName change separately, because it involves a settings + // object switch and we don't want to set the new name on the old settings. + changedSettings = await this.refreshSettings(printerName); + if (printerName != this.currentPrinterName) { + // Don't continue this update if the printer changed again. + return; + } + this.printForm.printerChanging = false; + this.printForm.enable(); + } else { + changedSettings = this.getSettingsToUpdate(); + } + + let shouldPreviewUpdate = + (await this.updateSettings( + changedSettings, + !!changedSettings.printerName + )) && previewableChange; + + if (shouldPreviewUpdate && !printPending) { + // We do not need to arm the preview task if the user has already printed + // and finalized any deferred tasks. + this.updatePrintPreview(); + } + document.dispatchEvent( + new CustomEvent("print-settings", { + detail: this.viewSettings, + }) + ); + }, + + async updateSettings(changedSettings = {}, printerChanged = false) { + let updatePreviewWithoutFlag = false; + let flags = 0; + logger.debug("updateSettings ", changedSettings, printerChanged); + + if (printerChanged || changedSettings.paperId) { + // The paper's margin properties are async, + // so resolve those now before we update the settings + try { + let paperWrapper = await PrintSettingsViewProxy.fetchPaperMargins( + changedSettings.paperId || this.viewSettings.paperId + ); + + // See if we also need to change the custom margin values + + let paperHeightInInches = paperWrapper.paper.height * INCHES_PER_POINT; + let paperWidthInInches = paperWrapper.paper.width * INCHES_PER_POINT; + let height = + (changedSettings.orientation || this.viewSettings.orientation) == 0 + ? paperHeightInInches + : paperWidthInInches; + let width = + (changedSettings.orientation || this.viewSettings.orientation) == 0 + ? paperWidthInInches + : paperHeightInInches; + + if ( + parseFloat(this.viewSettings.customMargins.marginTop) + + parseFloat(this.viewSettings.customMargins.marginBottom) > + height - + paperWrapper.unwriteableMarginTop - + paperWrapper.unwriteableMarginBottom || + this.viewSettings.customMargins.marginTop < 0 || + this.viewSettings.customMargins.marginBottom < 0 + ) { + let { marginTop, marginBottom } = this.viewSettings.defaultMargins; + changedSettings.marginTop = changedSettings.customMarginTop = marginTop; + changedSettings.marginBottom = changedSettings.customMarginBottom = marginBottom; + delete this._userChangedSettings.customMargins; + } + + if ( + parseFloat(this.viewSettings.customMargins.marginRight) + + parseFloat(this.viewSettings.customMargins.marginLeft) > + width - + paperWrapper.unwriteableMarginRight - + paperWrapper.unwriteableMarginLeft || + this.viewSettings.customMargins.marginLeft < 0 || + this.viewSettings.customMargins.marginRight < 0 + ) { + let { marginLeft, marginRight } = this.viewSettings.defaultMargins; + changedSettings.marginLeft = changedSettings.customMarginLeft = marginLeft; + changedSettings.marginRight = changedSettings.customMarginRight = marginRight; + delete this._userChangedSettings.customMargins; + } + } catch (e) { + this.reportPrintingError("PAPER_MARGINS"); + throw e; + } + } + + for (let [setting, value] of Object.entries(changedSettings)) { + // Always write paper changes back to settings as pref-derived values could be bad + if ( + this.viewSettings[setting] != value || + (printerChanged && setting == "paperId") + ) { + if (setting == "pageRanges") { + // The page range is kept as an array. If the user switches between all + // and custom with no specified range input (which is represented as an + // empty array), we do not want to send an update. + if (!this.viewSettings[setting].length && !value.length) { + continue; + } + } + this.viewSettings[setting] = value; + + if ( + setting in this.settingFlags && + setting in this._userChangedSettings + ) { + flags |= this.settingFlags[setting]; + } + updatePreviewWithoutFlag |= this._nonFlaggedUpdatePreviewSettings.has( + setting + ); + } + } + + let shouldPreviewUpdate = + flags || printerChanged || updatePreviewWithoutFlag; + logger.debug( + "updateSettings, calculated flags:", + flags, + "shouldPreviewUpdate:", + shouldPreviewUpdate + ); + if (flags) { + this.saveSettingsToPrefs(flags); + } + return shouldPreviewUpdate; + }, + + saveSettingsToPrefs(flags) { + PSSVC.savePrintSettingsToPrefs(this.settings, true, flags); + }, + + /** + * Queue a task to update the print preview. It will start immediately or when + * the in progress update completes. + */ + async updatePrintPreview() { + // Make sure the rendering state is set so we don't visibly update the + // sheet count with incomplete data. + this._showRenderingIndicator(); + this._updatePrintPreviewTask.arm(); + }, + + /** + * Creates a print preview or refreshes the preview with new settings when omitted. + * + * @return {Promise} Resolves when the preview has been updated. + */ + async _updatePrintPreview() { + let { settings } = this; + let { printSelectionOnly } = this.viewSettings; + if (!this.selectionPreviewBrowser) { + printSelectionOnly = false; + } + + // We never want the progress dialog to show + settings.showPrintProgress = false; + + this._showRenderingIndicator(); + + let sourceWinId; + + // If it's the first time loading this type of browser, get the stored window id. + if (printSelectionOnly && !this._hasRenderedSelectionPreview) { + sourceWinId = this.selectionWindowId; + this._hasRenderedSelectionPreview = true; + } else if (!printSelectionOnly && !this._hasRenderedPrimaryPreview) { + sourceWinId = this.sourceWindowId; + this._hasRenderedPrimaryPreview = true; + } + + this.previewBrowser.parentElement.setAttribute( + "previewtype", + printSelectionOnly ? "selection" : "primary" + ); + + this.currentPreviewBrowser = printSelectionOnly + ? this.selectionPreviewBrowser + : this.previewBrowser; + + const isFirstCall = !this.printInitiationTime; + if (isFirstCall) { + let params = new URLSearchParams(location.search); + this.printInitiationTime = parseInt( + params.get("printInitiationTime"), + 10 + ); + const elapsed = Date.now() - this.printInitiationTime; + Services.telemetry + .getHistogramById("PRINT_INIT_TO_PLATFORM_SENT_SETTINGS_MS") + .add(elapsed); + } + + let totalPageCount, sheetCount, isEmpty; + try { + // This resolves with a PrintPreviewSuccessInfo dictionary. + ({ + totalPageCount, + sheetCount, + isEmpty, + } = await this.currentPreviewBrowser.frameLoader.printPreview( + settings, + sourceWinId + )); + } catch (e) { + this.reportPrintingError("PRINT_PREVIEW"); + throw e; + } + + this.previewIsEmpty = isEmpty; + // If the preview is empty, we know our range is greater than the number of pages. + // We have to send a pageRange update to display a non-empty page. + if (this.previewIsEmpty) { + this.viewSettings.pageRanges = []; + this.updatePrintPreview(); + } + + // Update the settings print options on whether there is a selection. + settings.isPrintSelectionRBEnabled = this.hasSelection; + + document.dispatchEvent( + new CustomEvent("page-count", { + detail: { sheetCount, totalPages: totalPageCount }, + }) + ); + this.currentPreviewBrowser.setAttribute("sheet-count", sheetCount); + + this._hideRenderingIndicator(); + + if (isFirstCall) { + const elapsed = Date.now() - this.printInitiationTime; + Services.telemetry + .getHistogramById("PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS") + .add(elapsed); + } + }, + + _showRenderingIndicator() { + let stack = this.previewBrowser.parentElement; + stack.setAttribute("rendering", true); + document.body.setAttribute("rendering", true); + }, + + _hideRenderingIndicator() { + let stack = this.previewBrowser.parentElement; + stack.removeAttribute("rendering"); + document.body.removeAttribute("rendering"); + }, + + getSourceBrowsingContext() { + let params = new URLSearchParams(location.search); + let browsingContextId = params.get("browsingContextId"); + if (!browsingContextId) { + return null; + } + return BrowsingContext.get(browsingContextId); + }, + + async getPrintDestinations() { + const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance( + Ci.nsIPrinterList + ); + let printers; + + if (Cu.isInAutomation) { + printers = await Promise.resolve(window._mockPrinters || []); + } else { + try { + printers = await printerList.printers; + } catch (e) { + this.reportPrintingError("PRINTER_LIST"); + throw e; + } + } + + let fallbackPaperList; + try { + fallbackPaperList = await printerList.fallbackPaperList; + } catch (e) { + this.reportPrintingError("FALLBACK_PAPER_LIST"); + throw e; + } + + let lastUsedPrinterName; + try { + lastUsedPrinterName = PSSVC.lastUsedPrinterName; + } catch (e) { + this.reportPrintingError("LAST_USED_PRINTER"); + throw e; + } + const defaultPrinterName = printerList.systemDefaultPrinterName; + const printersByName = {}; + + let lastUsedPrinter; + let defaultSystemPrinter; + + let saveToPdfPrinter = { + nameId: "printui-destination-pdf-label", + value: PrintUtils.SAVE_TO_PDF_PRINTER, + }; + printersByName[PrintUtils.SAVE_TO_PDF_PRINTER] = { + supportsColor: true, + supportsMonochrome: false, + name: PrintUtils.SAVE_TO_PDF_PRINTER, + }; + + if (lastUsedPrinterName == PrintUtils.SAVE_TO_PDF_PRINTER) { + lastUsedPrinter = saveToPdfPrinter; + } + + let destinations = [ + saveToPdfPrinter, + ...printers.map(printer => { + printer.QueryInterface(Ci.nsIPrinter); + const { name } = printer; + printersByName[printer.name] = { printer }; + const destination = { name, value: name }; + + if (name == lastUsedPrinterName) { + lastUsedPrinter = destination; + } + if (name == defaultPrinterName) { + defaultSystemPrinter = destination; + } + + return destination; + }), + ]; + + let selectedPrinter = + lastUsedPrinter || defaultSystemPrinter || saveToPdfPrinter; + + return { + destinations, + fallbackPaperList, + selectedPrinter, + printersByName, + defaultSystemPrinter, + }; + }, + + getMarginPresets(marginSize, paper) { + switch (marginSize) { + case "minimum": + return { + marginTop: paper.unwriteableMarginTop, + marginLeft: paper.unwriteableMarginLeft, + marginBottom: paper.unwriteableMarginBottom, + marginRight: paper.unwriteableMarginRight, + }; + case "none": + return { + marginTop: 0, + marginLeft: 0, + marginBottom: 0, + marginRight: 0, + }; + case "custom": + return { + marginTop: + PrintSettingsViewProxy._lastCustomMarginValues.marginTop ?? + this.settings.marginTop, + marginBottom: + PrintSettingsViewProxy._lastCustomMarginValues.marginBottom ?? + this.settings.marginBottom, + marginLeft: + PrintSettingsViewProxy._lastCustomMarginValues.marginLeft ?? + this.settings.marginLeft, + marginRight: + PrintSettingsViewProxy._lastCustomMarginValues.marginRight ?? + this.settings.marginRight, + }; + default: { + let minimum = this.getMarginPresets("minimum", paper); + return { + marginTop: !isNaN(minimum.marginTop) + ? Math.max(minimum.marginTop, this.defaultSettings.marginTop) + : this.defaultSettings.marginTop, + marginRight: !isNaN(minimum.marginRight) + ? Math.max(minimum.marginRight, this.defaultSettings.marginRight) + : this.defaultSettings.marginRight, + marginBottom: !isNaN(minimum.marginBottom) + ? Math.max(minimum.marginBottom, this.defaultSettings.marginBottom) + : this.defaultSettings.marginBottom, + marginLeft: !isNaN(minimum.marginLeft) + ? Math.max(minimum.marginLeft, this.defaultSettings.marginLeft) + : this.defaultSettings.marginLeft, + }; + } + } + }, + + reportPrintingError(aMessage) { + Services.telemetry.keyedScalarAdd("printing.error", aMessage, 1); + }, + + /** + * Prints the window. This method has been abstracted into a helper for + * testing purposes. + */ + _doPrint(aBrowsingContext, aSettings) { + return aBrowsingContext.top.embedderElement.print( + aBrowsingContext.currentWindowGlobal.outerWindowId, + aSettings + ); + }, + + /** + * Shows the system dialog. This method has been abstracted into a helper for + * testing purposes. The showPrintDialog() call blocks until the dialog is + * closed, so we mark it as async to allow us to reject from the test. + */ + async _showPrintDialog(aPrintingPromptService, aWindow, aSettings) { + return aPrintingPromptService.showPrintDialog(aWindow, aSettings); + }, +}; + +var PrintSettingsViewProxy = { + get defaultHeadersAndFooterValues() { + const defaultBranch = Services.prefs.getDefaultBranch(""); + let settingValues = {}; + for (let [name, pref] of Object.entries(this.headerFooterSettingsPrefs)) { + settingValues[name] = defaultBranch.getStringPref(pref); + } + // We only need to retrieve these defaults once and they will not change + Object.defineProperty(this, "defaultHeadersAndFooterValues", { + value: settingValues, + }); + return settingValues; + }, + + headerFooterSettingsPrefs: { + footerStrCenter: "print.print_footercenter", + footerStrLeft: "print.print_footerleft", + footerStrRight: "print.print_footerright", + headerStrCenter: "print.print_headercenter", + headerStrLeft: "print.print_headerleft", + headerStrRight: "print.print_headerright", + }, + + // Custom margins are not saved by a pref, so we need to keep track of them + // in order to save the value. + _lastCustomMarginValues: { + marginTop: null, + marginBottom: null, + marginLeft: null, + marginRight: null, + }, + + // This list was taken from nsDeviceContextSpecWin.cpp which records telemetry on print target type + knownSaveToFilePrinters: new Set([ + "Microsoft Print to PDF", + "Adobe PDF", + "Bullzip PDF Printer", + "CutePDF Writer", + "doPDF", + "Foxit Reader PDF Printer", + "Nitro PDF Creator", + "novaPDF", + "PDF-XChange", + "PDF24 PDF", + "PDFCreator", + "PrimoPDF", + "Soda PDF", + "Solid PDF Creator", + "Universal Document Converter", + "Microsoft XPS Document Writer", + ]), + + getBestPaperMatch(paperWidth, paperHeight, paperSizeUnit) { + let paperSizes = Object.values(this.availablePaperSizes); + if (!(paperWidth && paperHeight)) { + return null; + } + // first try to match on the paper dimensions using the current units + let unitsPerPoint; + let altUnitsPerPoint; + if (paperSizeUnit == PrintEventHandler.settings.kPaperSizeMillimeters) { + unitsPerPoint = MM_PER_POINT; + altUnitsPerPoint = INCHES_PER_POINT; + } else { + unitsPerPoint = INCHES_PER_POINT; + altUnitsPerPoint = MM_PER_POINT; + } + // equality to 1pt. + const equal = (a, b) => Math.abs(a - b) < 1; + const findMatch = (widthPts, heightPts) => + paperSizes.find(paperWrapper => { + // the dimensions on the nsIPaper object are in points + let result = + equal(widthPts, paperWrapper.paper.width) && + equal(heightPts, paperWrapper.paper.height); + return result; + }); + // Look for a paper with matching dimensions, using the current printer's + // paper size unit, then the alternate unit + let matchedPaper = + findMatch(paperWidth / unitsPerPoint, paperHeight / unitsPerPoint) || + findMatch(paperWidth / altUnitsPerPoint, paperHeight / altUnitsPerPoint); + + if (matchedPaper) { + return matchedPaper; + } + return null; + }, + + async fetchPaperMargins(paperId) { + // resolve any async and computed properties we need on the paper + let paperWrapper = this.availablePaperSizes[paperId]; + if (!paperWrapper) { + throw new Error("Can't fetchPaperMargins: " + paperId); + } + if (paperWrapper._resolved) { + // We've already resolved and calculated these values + return paperWrapper; + } + let margins; + try { + margins = await paperWrapper.paper.unwriteableMargin; + } catch (e) { + this.reportPrintingError("UNWRITEABLE_MARGIN"); + throw e; + } + margins.QueryInterface(Ci.nsIPaperMargin); + + // margin dimensions are given on the paper in points, setting values need to be in inches + paperWrapper.unwriteableMarginTop = margins.top * INCHES_PER_POINT; + paperWrapper.unwriteableMarginRight = margins.right * INCHES_PER_POINT; + paperWrapper.unwriteableMarginBottom = margins.bottom * INCHES_PER_POINT; + paperWrapper.unwriteableMarginLeft = margins.left * INCHES_PER_POINT; + // No need to re-resolve static properties + paperWrapper._resolved = true; + return paperWrapper; + }, + + async resolvePropertiesForPrinter(printerName) { + // resolve any async properties we need on the printer + let printerInfo = this.availablePrinters[printerName]; + if (printerInfo._resolved) { + // Store a convenience reference + this.availablePaperSizes = printerInfo.availablePaperSizes; + return printerInfo; + } + + // Await the async printer data. + if (printerInfo.printer) { + let basePrinterInfo; + try { + [ + printerInfo.supportsDuplex, + printerInfo.supportsColor, + printerInfo.supportsMonochrome, + basePrinterInfo, + ] = await Promise.all([ + printerInfo.printer.supportsDuplex, + printerInfo.printer.supportsColor, + printerInfo.printer.supportsMonochrome, + printerInfo.printer.printerInfo, + ]); + } catch (e) { + this.reportPrintingError("PRINTER_SETTINGS"); + throw e; + } + basePrinterInfo.QueryInterface(Ci.nsIPrinterInfo); + basePrinterInfo.defaultSettings.QueryInterface(Ci.nsIPrintSettings); + + printerInfo.paperList = basePrinterInfo.paperList; + printerInfo.defaultSettings = basePrinterInfo.defaultSettings; + } else if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { + // The Mozilla PDF pseudo-printer has no actual nsIPrinter implementation + printerInfo.defaultSettings = PSSVC.newPrintSettings; + printerInfo.defaultSettings.printerName = printerName; + printerInfo.defaultSettings.toFileName = ""; + printerInfo.defaultSettings.outputFormat = + Ci.nsIPrintSettings.kOutputFormatPDF; + printerInfo.defaultSettings.printToFile = true; + printerInfo.paperList = this.fallbackPaperList; + } + printerInfo.settings = printerInfo.defaultSettings.clone(); + // Apply any previously persisted user values + let flags = printerInfo.settings.kInitSaveAll; + if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { + // Don't apply potentially-bad printToFile setting that may be in some user's prefs. + flags ^= printerInfo.settings.kInitSavePrintToFile; + } + PSSVC.initPrintSettingsFromPrefs(printerInfo.settings, true, flags); + // We set `isInitializedFromPrinter` to make sure that that's set on the + // SAVE_TO_PDF_PRINTER settings. The naming is poor, but that tells the + // platform code that the settings object is complete. + printerInfo.settings.isInitializedFromPrinter = true; + + printerInfo.settings.toFileName = ""; + + // prepare the available paper sizes for this printer + if (!printerInfo.paperList?.length) { + logger.warn( + "Printer has empty paperList: ", + printerInfo.printer.id, + "using fallbackPaperList" + ); + printerInfo.paperList = this.fallbackPaperList; + } + // don't trust the settings to provide valid paperSizeUnit values + let sizeUnit = + printerInfo.settings.paperSizeUnit == + printerInfo.settings.kPaperSizeMillimeters + ? printerInfo.settings.kPaperSizeMillimeters + : printerInfo.settings.kPaperSizeInches; + let papersById = (printerInfo.availablePaperSizes = {}); + // Store a convenience reference + this.availablePaperSizes = papersById; + + for (let paper of printerInfo.paperList) { + paper.QueryInterface(Ci.nsIPaper); + // Bug 1662239: I'm seeing multiple duplicate entries for each paper size + // so ensure we have one entry per name + if (!papersById[paper.id]) { + papersById[paper.id] = { + paper, + id: paper.id, + name: paper.name, + // XXXsfoster: Eventually we want to get the unit from the nsIPaper object + sizeUnit, + }; + } + } + // Update our cache of all the paper sizes by name + Object.assign(PrintEventHandler.allPaperSizes, papersById); + + // The printer properties don't change, mark this as resolved for next time + printerInfo._resolved = true; + return printerInfo; + }, + + get(target, name) { + switch (name) { + case "currentPaper": { + let paperId = this.get(target, "paperId"); + return paperId && this.availablePaperSizes[paperId]; + } + + case "marginPresets": + let paperWrapper = this.get(target, "currentPaper"); + return { + none: PrintEventHandler.getMarginPresets("none", paperWrapper), + minimum: PrintEventHandler.getMarginPresets("minimum", paperWrapper), + default: PrintEventHandler.getMarginPresets("default", paperWrapper), + custom: PrintEventHandler.getMarginPresets("custom", paperWrapper), + }; + + case "marginOptions": { + let allMarginPresets = this.get(target, "marginPresets"); + let uniqueMargins = new Set(); + let marginsEnabled = {}; + for (let name of ["none", "default", "minimum", "custom"]) { + let { + marginTop, + marginLeft, + marginBottom, + marginRight, + } = allMarginPresets[name]; + let key = [marginTop, marginLeft, marginBottom, marginRight].join( + "," + ); + // Custom margins are initialized to default margins + marginsEnabled[name] = !uniqueMargins.has(key) || name == "custom"; + uniqueMargins.add(key); + } + return marginsEnabled; + } + + case "margins": + let marginSettings = { + marginTop: target.marginTop, + marginLeft: target.marginLeft, + marginBottom: target.marginBottom, + marginRight: target.marginRight, + }; + // see if they match the none, minimum, or default margin values + let allMarginPresets = this.get(target, "marginPresets"); + for (let presetName of ["none", "minimum", "default"]) { + let marginPresets = allMarginPresets[presetName]; + if ( + Object.keys(marginSettings).every( + name => + marginSettings[name].toFixed(2) == + marginPresets[name].toFixed(2) + ) + ) { + return presetName; + } + } + + // Fall back to custom for other values + return "custom"; + + case "defaultMargins": + return PrintEventHandler.getMarginPresets( + "default", + this.get(target, "currentPaper") + ); + + case "customMargins": + return PrintEventHandler.getMarginPresets( + "custom", + this.get(target, "currentPaper") + ); + + case "paperSizes": + return Object.values(this.availablePaperSizes) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(paper => { + return { + name: paper.name, + value: paper.id, + }; + }); + + case "supportsDuplex": + return this.availablePrinters[target.printerName].supportsDuplex; + + case "printDuplex": + return target.duplex; + + case "printBackgrounds": + return target.printBGImages || target.printBGColors; + + case "printFootersHeaders": + // if any of the footer and headers settings have a non-empty string value + // we consider that "enabled" + return Object.keys(this.headerFooterSettingsPrefs).some( + name => !!target[name] + ); + + case "supportsColor": + return this.availablePrinters[target.printerName].supportsColor; + + case "willSaveToFile": + return ( + target.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF || + this.knownSaveToFilePrinters.has(target.printerName) + ); + case "supportsMonochrome": + return this.availablePrinters[target.printerName].supportsMonochrome; + case "defaultSystemPrinter": + return ( + this.defaultSystemPrinter?.value || + Object.getOwnPropertyNames(this.availablePrinters).find( + name => name != PrintUtils.SAVE_TO_PDF_PRINTER + ) + ); + + case "numCopies": + return this.get(target, "willSaveToFile") ? 1 : target.numCopies; + } + return target[name]; + }, + + set(target, name, value) { + switch (name) { + case "margins": + if (!["default", "minimum", "none", "custom"].includes(value)) { + logger.warn("Unexpected margin preset name: ", value); + value = "default"; + } + let paperWrapper = this.get(target, "currentPaper"); + let marginPresets = PrintEventHandler.getMarginPresets( + value, + paperWrapper + ); + for (let [settingName, presetValue] of Object.entries(marginPresets)) { + target[settingName] = presetValue; + } + break; + + case "paperId": { + let paperId = value; + let paperWrapper = this.availablePaperSizes[paperId]; + // Dimensions on the paper object are in pts. + // We convert to the printer's specified unit when updating settings + let unitsPerPoint = + paperWrapper.sizeUnit == target.kPaperSizeMillimeters + ? MM_PER_POINT + : INCHES_PER_POINT; + // paperWidth and paperHeight are calculated values that we always treat as suspect and + // re-calculate whenever the paperId changes + target.paperSizeUnit = paperWrapper.sizeUnit; + target.paperWidth = paperWrapper.paper.width * unitsPerPoint; + target.paperHeight = paperWrapper.paper.height * unitsPerPoint; + // Unwriteable margins were pre-calculated from their async values when the paper size + // was selected. They are always in inches + target.unwriteableMarginTop = paperWrapper.unwriteableMarginTop; + target.unwriteableMarginRight = paperWrapper.unwriteableMarginRight; + target.unwriteableMarginBottom = paperWrapper.unwriteableMarginBottom; + target.unwriteableMarginLeft = paperWrapper.unwriteableMarginLeft; + target.paperId = paperWrapper.paper.id; + // pull new margin values for the new paper size + this.set(target, "margins", this.get(target, "margins")); + break; + } + + case "printerName": + // Can't set printerName, settings objects belong to a specific printer. + break; + + case "printBackgrounds": + target.printBGImages = value; + target.printBGColors = value; + break; + + case "printDuplex": + target.duplex = value + ? Ci.nsIPrintSettings.kDuplexHorizontal + : Ci.nsIPrintSettings.kSimplex; + break; + + case "printFootersHeaders": + // To disable header & footers, set them all to empty. + // To enable, restore default values for each of the header & footer settings. + for (let [settingName, defaultValue] of Object.entries( + this.defaultHeadersAndFooterValues + )) { + target[settingName] = value ? defaultValue : ""; + } + break; + + case "customMargins": + if (value != null) { + for (let [settingName, newVal] of Object.entries(value)) { + target[settingName] = newVal; + this._lastCustomMarginValues[settingName] = newVal; + } + } + break; + + case "customMarginTop": + case "customMarginBottom": + case "customMarginLeft": + case "customMarginRight": + let customMarginName = "margin" + name.substring(12); + this.set( + target, + "customMargins", + Object.assign({}, this.get(target, "customMargins"), { + [customMarginName]: value, + }) + ); + break; + + default: + target[name] = value; + } + }, +}; + +/* + * Custom elements ---------------------------------------------------- + */ + +function PrintUIControlMixin(superClass) { + return class PrintUIControl extends superClass { + connectedCallback() { + this.setAttribute("autocomplete", "off"); + this.initialize(); + this.render(); + } + + initialize() { + if (this._initialized) { + return; + } + this._initialized = true; + if (this.templateId) { + let template = this.ownerDocument.getElementById(this.templateId); + let templateContent = template.content; + this.appendChild(templateContent.cloneNode(true)); + } + + document.addEventListener("print-settings", ({ detail: settings }) => { + this.update(settings); + }); + + this.addEventListener("input", this); + } + + render() {} + + update(settings) {} + + dispatchSettingsChange(changedSettings) { + this.dispatchEvent( + new CustomEvent("update-print-settings", { + bubbles: true, + detail: changedSettings, + }) + ); + } + + cancelSettingsChange(changedSettings) { + this.dispatchEvent( + new CustomEvent("cancel-print-settings", { + bubbles: true, + detail: changedSettings, + }) + ); + } + + handleEvent(event) {} + }; +} + +class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) { + initialize() { + super.initialize(); + + this.addEventListener("submit", this); + this.addEventListener("click", this); + this.addEventListener("revalidate", this); + + this._printerDestination = this.querySelector("#destination"); + + this.printButton = this.querySelector("#print-button"); + if (AppConstants.platform != "win") { + // Move the Print button to the end if this isn't Windows. + this.printButton.parentElement.append(this.printButton); + } + this.querySelector("#pages-per-sheet").hidden = !Services.prefs.getBoolPref( + "print.pages_per_sheet.enabled", + false + ); + } + + removeNonPdfSettings() { + let selectors = [ + "#margins", + "#headers-footers", + "#backgrounds", + "#print-selection-container", + ]; + for (let selector of selectors) { + this.querySelector(selector).remove(); + } + let moreSettings = this.querySelector("#more-settings-options"); + if (moreSettings.children.length <= 1) { + moreSettings.remove(); + } + } + + requestPrint() { + this.requestSubmit(this.printButton); + } + + update(settings) { + // If there are no default system printers available and we are not on mac, + // we should hide the system dialog because it won't be populated with + // the correct settings. Mac and Gtk support save to pdf functionality + // in the native dialog, so it can be shown regardless. + this.querySelector("#system-print").hidden = + AppConstants.platform === "win" && !settings.defaultSystemPrinter; + + this.querySelector("#copies").hidden = settings.willSaveToFile; + + this.querySelector("#two-sided-printing").hidden = !settings.supportsDuplex; + } + + enable() { + let isValid = this.checkValidity(); + document.body.toggleAttribute("invalid", !isValid); + if (isValid) { + for (let element of this.elements) { + if (!element.hasAttribute("disallowed")) { + element.disabled = false; + } + } + // aria-describedby will usually cause the first value to be reported. + // Unfortunately, screen readers don't pick up description changes from + // dialogs, so we must use a live region. To avoid double reporting of + // the first value, we don't set aria-live initially. We only set it for + // subsequent updates. + // aria-live is set on the parent because sheetCount itself might be + // hidden and then shown, and updates are only reported for live + // regions that were already visible. + document + .querySelector("#sheet-count") + .parentNode.setAttribute("aria-live", "polite"); + } else { + // Find the invalid element + let invalidElement; + for (let element of this.elements) { + if (!element.checkValidity()) { + invalidElement = element; + break; + } + } + let section = invalidElement.closest(".section-block"); + document.body.toggleAttribute("invalid", !isValid); + // We're hiding the sheet count and aria-describedby includes the + // content of hidden elements, so remove aria-describedby. + document.body.removeAttribute("aria-describedby"); + for (let element of this.elements) { + // If we're valid, enable all inputs. + // Otherwise, disable the valid inputs other than the cancel button and the elements + // in the invalid section. + element.disabled = + element.hasAttribute("disallowed") || + (!isValid && + element.validity.valid && + element.name != "cancel" && + element.closest(".section-block") != this._printerDestination && + element.closest(".section-block") != section); + } + } + } + + disable(filterFn) { + for (let element of this.elements) { + if (filterFn && !filterFn(element)) { + continue; + } + element.disabled = element.name != "cancel"; + } + } + + handleEvent(e) { + if (e.target.id == "open-dialog-link") { + this.dispatchEvent(new Event("open-system-dialog", { bubbles: true })); + return; + } + + if (e.type == "submit") { + e.preventDefault(); + if (e.submitter.name == "print" && this.checkValidity()) { + this.dispatchEvent(new Event("print", { bubbles: true })); + } + } else if ( + (e.type == "input" || e.type == "revalidate") && + !this.printerChanging + ) { + this.enable(); + } + } +} +customElements.define("print-form", PrintUIForm, { extends: "form" }); + +class PrintSettingSelect extends PrintUIControlMixin(HTMLSelectElement) { + initialize() { + super.initialize(); + this.addEventListener("keypress", this); + } + + connectedCallback() { + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + setOptions(optionValues = []) { + this.textContent = ""; + for (let optionData of optionValues) { + let opt = new Option( + optionData.name, + "value" in optionData ? optionData.value : optionData.name + ); + if (optionData.nameId) { + document.l10n.setAttributes(opt, optionData.nameId); + } + // option selectedness is set via update() and assignment to this.value + this.options.add(opt); + } + } + + update(settings) { + if (this.settingName) { + this.value = settings[this.settingName]; + } + } + + handleEvent(e) { + if (e.type == "input" && this.settingName) { + this.dispatchSettingsChange({ + [this.settingName]: e.target.value, + }); + } else if (e.type == "keypress") { + if ( + e.key == "Enter" && + (!e.metaKey || AppConstants.platform == "macosx") + ) { + this.form.requestPrint(); + } + } + } +} +customElements.define("setting-select", PrintSettingSelect, { + extends: "select", +}); + +class PrintSettingNumber extends PrintUIControlMixin(HTMLInputElement) { + initialize() { + super.initialize(); + this.addEventListener("keypress", e => this.handleKeypress(e)); + this.addEventListener("paste", e => this.handlePaste(e)); + } + + connectedCallback() { + this.type = "number"; + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + update(settings) { + if (this.settingName) { + this.value = settings[this.settingName]; + } + } + + handleKeypress(e) { + let char = String.fromCharCode(e.charCode); + let acceptedChar = e.target.step.includes(".") + ? char.match(/^[0-9.]$/) + : char.match(/^[0-9]$/); + if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + } + } + + handlePaste(e) { + let paste = (e.clipboardData || window.clipboardData) + .getData("text") + .trim(); + let acceptedChars = e.target.step.includes(".") + ? paste.match(/^[0-9.]*$/) + : paste.match(/^[0-9]*$/); + if (!acceptedChars) { + e.preventDefault(); + } + } + + handleEvent(e) { + switch (e.type) { + case "paste": + this.handlePaste(); + break; + case "keypress": + this.handleKeypress(); + break; + case "input": + if (this.settingName && this.checkValidity()) { + this.dispatchSettingsChange({ + [this.settingName]: this.value, + }); + } + break; + } + } +} +customElements.define("setting-number", PrintSettingNumber, { + extends: "input", +}); + +class PrintSettingCheckbox extends PrintUIControlMixin(HTMLInputElement) { + connectedCallback() { + this.type = "checkbox"; + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + update(settings) { + this.checked = settings[this.settingName]; + } + + handleEvent(e) { + this.dispatchSettingsChange({ + [this.settingName]: this.checked, + }); + } +} +customElements.define("setting-checkbox", PrintSettingCheckbox, { + extends: "input", +}); + +class DestinationPicker extends PrintSettingSelect { + initialize() { + super.initialize(); + document.addEventListener("available-destinations", this); + } + + update(settings) { + super.update(settings); + let isPdf = settings.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF; + this.setAttribute("output", isPdf ? "pdf" : "paper"); + } + + handleEvent(e) { + super.handleEvent(e); + + if (e.type == "available-destinations") { + this.setOptions(e.detail); + } + } +} +customElements.define("destination-picker", DestinationPicker, { + extends: "select", +}); + +class ColorModePicker extends PrintSettingSelect { + update(settings) { + this.value = settings[this.settingName] ? "color" : "bw"; + let canSwitch = settings.supportsColor && settings.supportsMonochrome; + if (this.disablePicker != canSwitch) { + this.toggleAttribute("disallowed", !canSwitch); + this.disabled = !canSwitch; + } + this.disablePicker = canSwitch; + } + + handleEvent(e) { + if (e.type == "input") { + // turn our string value into the expected boolean + this.dispatchSettingsChange({ + [this.settingName]: this.value == "color", + }); + } + } +} +customElements.define("color-mode-select", ColorModePicker, { + extends: "select", +}); + +class PaperSizePicker extends PrintSettingSelect { + initialize() { + super.initialize(); + this._printerName = null; + } + + update(settings) { + if (settings.printerName !== this._printerName) { + this._printerName = settings.printerName; + this.setOptions(settings.paperSizes); + } + this.value = settings.paperId; + } +} +customElements.define("paper-size-select", PaperSizePicker, { + extends: "select", +}); + +class OrientationInput extends PrintUIControlMixin(HTMLElement) { + get templateId() { + return "orientation-template"; + } + + update(settings) { + for (let input of this.querySelectorAll("input")) { + input.checked = settings.orientation == input.value; + } + } + + handleEvent(e) { + this.dispatchSettingsChange({ + orientation: e.target.value, + }); + } +} +customElements.define("orientation-input", OrientationInput); + +class ScaleInput extends PrintUIControlMixin(HTMLElement) { + get templateId() { + return "scale-template"; + } + + initialize() { + super.initialize(); + + this._percentScale = this.querySelector("#percent-scale"); + this._shrinkToFitChoice = this.querySelector("#fit-choice"); + this._scaleChoice = this.querySelector("#percent-scale-choice"); + this._scaleError = this.querySelector("#error-invalid-scale"); + } + + updateScale() { + this.dispatchSettingsChange({ + scaling: Number(this._percentScale.value / 100), + }); + } + + update(settings) { + let { scaling, shrinkToFit, printerName } = settings; + this._shrinkToFitChoice.checked = shrinkToFit; + this._scaleChoice.checked = !shrinkToFit; + if (this.disableScale != shrinkToFit) { + this._percentScale.disabled = shrinkToFit; + this._percentScale.toggleAttribute("disallowed", shrinkToFit); + } + this.disableScale = shrinkToFit; + if (!this.printerName) { + this.printerName = printerName; + } + + // If the user had an invalid input and switches back to "fit to page", + // we repopulate the scale field with the stored, valid scaling value. + let isValid = this._percentScale.checkValidity(); + if ( + !this._percentScale.value || + (this._shrinkToFitChoice.checked && !isValid) || + (this.printerName != printerName && !isValid) + ) { + // Only allow whole numbers. 0.14 * 100 would have decimal places, etc. + this._percentScale.value = parseInt(scaling * 100, 10); + this.printerName = printerName; + if (!isValid) { + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + this._scaleError.hidden = true; + } + } + } + + handleEvent(e) { + if (e.target == this._shrinkToFitChoice || e.target == this._scaleChoice) { + if (!this._percentScale.checkValidity()) { + this._percentScale.value = 100; + } + let scale = + e.target == this._shrinkToFitChoice + ? 1 + : Number(this._percentScale.value / 100); + this.dispatchSettingsChange({ + shrinkToFit: this._shrinkToFitChoice.checked, + scaling: scale, + }); + this._scaleError.hidden = true; + } else if (e.type == "input") { + if (this._percentScale.checkValidity()) { + this.updateScale(); + } + } + + window.clearTimeout(this.showErrorTimeoutId); + if (this._percentScale.validity.valid) { + this._scaleError.hidden = true; + } else { + this.cancelSettingsChange({ scaling: true }); + this.showErrorTimeoutId = window.setTimeout(() => { + this._scaleError.hidden = false; + }, INPUT_DELAY_MS); + } + } +} +customElements.define("scale-input", ScaleInput); + +class PageRangeInput extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + + this._rangeInput = this.querySelector("#custom-range"); + this._rangeInput.title = ""; + this._rangePicker = this.querySelector("#range-picker"); + this._rangeError = this.querySelector("#error-invalid-range"); + this._startRangeOverflowError = this.querySelector( + "#error-invalid-start-range-overflow" + ); + + this._pagesSet = new Set(); + + this.addEventListener("keypress", this); + this.addEventListener("paste", this); + document.addEventListener("page-count", this); + } + + get templateId() { + return "page-range-template"; + } + + updatePageRange() { + let isAll = this._rangePicker.value == "all"; + if (isAll) { + this._pagesSet.clear(); + if (!this._rangeInput.checkValidity()) { + this._rangeInput.setCustomValidity(""); + this._rangeInput.value = ""; + } + } else { + this.validateRangeInput(); + } + + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + + document.l10n.setAttributes( + this._rangeError, + "printui-error-invalid-range", + { + numPages: this._numPages, + } + ); + + // If it's valid, update the page range and hide the error messages. + // Otherwise, set the appropriate error message + if (this._rangeInput.validity.valid || isAll) { + window.clearTimeout(this.showErrorTimeoutId); + this._startRangeOverflowError.hidden = this._rangeError.hidden = true; + } else { + this._rangeInput.focus(); + } + } + + dispatchPageRange(shouldCancel = true) { + window.clearTimeout(this.showErrorTimeoutId); + if (this._rangeInput.validity.valid || this._rangePicker.value == "all") { + this.dispatchSettingsChange({ + pageRanges: this.formatPageRange(), + }); + } else { + if (shouldCancel) { + this.cancelSettingsChange({ pageRanges: true }); + } + this.showErrorTimeoutId = window.setTimeout(() => { + this._rangeError.hidden = + this._rangeInput.validationMessage != "invalid"; + this._startRangeOverflowError.hidden = + this._rangeInput.validationMessage != "startRangeOverflow"; + }, INPUT_DELAY_MS); + } + } + + // The platform expects pageRanges to be an array of + // ranges represented by ints. + // Ex: Printing pages 1-3 would return [1,3] + // Ex: Printing page 1 would return [1,1] + // Ex: Printing pages 1-2,4 would return [1,2,4,4] + formatPageRange() { + if ( + this._pagesSet.size == 0 || + this._rangeInput.value == "" || + this._rangePicker.value == "all" + ) { + // Show all pages. + return []; + } + let pages = Array.from(this._pagesSet).sort((a, b) => a - b); + + let formattedRanges = []; + let startRange = pages[0]; + let endRange = pages[0]; + formattedRanges.push(startRange); + + for (let i = 1; i < pages.length; i++) { + let currentPage = pages[i - 1]; + let nextPage = pages[i]; + if (nextPage > currentPage + 1) { + formattedRanges.push(endRange); + startRange = endRange = nextPage; + formattedRanges.push(startRange); + } else { + endRange = nextPage; + } + } + formattedRanges.push(endRange); + + return formattedRanges; + } + + update(settings) { + let { pageRanges, printerName } = settings; + this.toggleAttribute("all-pages", !pageRanges.length); + if (!this.printerName) { + this.printerName = printerName; + } + + let isValid = this._rangeInput.checkValidity(); + if (this.printerName != printerName && !isValid) { + this.printerName = printerName; + this._rangeInput.value = ""; + this.updatePageRange(); + this.dispatchPageRange(); + } + } + + handleKeypress(e) { + let char = String.fromCharCode(e.charCode); + let acceptedChar = char.match(/^[0-9,-]$/); + if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + } + } + + handlePaste(e) { + let paste = (e.clipboardData || window.clipboardData) + .getData("text") + .trim(); + if (!paste.match(/^[0-9,-]*$/)) { + e.preventDefault(); + } + } + + // This method has been abstracted into a helper for testing purposes + _validateRangeInput(value, numPages) { + this._pagesSet.clear(); + var ranges = value.split(","); + + for (let range of ranges) { + let rangeParts = range.split("-"); + if (rangeParts.length > 2) { + this._rangeInput.setCustomValidity("invalid"); + this._rangeInput.title = ""; + this._pagesSet.clear(); + return; + } + let startRange = parseInt(rangeParts[0], 10); + let endRange = parseInt( + rangeParts.length == 2 ? rangeParts[1] : rangeParts[0], + 10 + ); + + if (isNaN(startRange) && isNaN(endRange)) { + continue; + } + + // If the startRange was not specified, then we infer this + // to be 1. + if (isNaN(startRange) && rangeParts[0] == "") { + startRange = 1; + } + // If the end range was not specified, then we infer this + // to be the total number of pages. + if (isNaN(endRange) && rangeParts[1] == "") { + endRange = numPages; + } + + // Check the range for errors + if (endRange < startRange) { + this._rangeInput.setCustomValidity("startRangeOverflow"); + this._pagesSet.clear(); + return; + } else if ( + startRange > numPages || + endRange > numPages || + startRange == 0 + ) { + this._rangeInput.setCustomValidity("invalid"); + this._rangeInput.title = ""; + this._pagesSet.clear(); + return; + } + + for (let i = startRange; i <= endRange; i++) { + this._pagesSet.add(i); + } + } + + this._rangeInput.setCustomValidity(""); + } + + validateRangeInput() { + let value = this._rangePicker.value == "all" ? "" : this._rangeInput.value; + this._validateRangeInput(value, this._numPages); + } + + handleEvent(e) { + if (e.type == "keypress") { + if (e.target == this._rangeInput) { + this.handleKeypress(e); + } + return; + } + + if (e.type === "paste" && e.target == this._rangeInput) { + this.handlePaste(e); + return; + } + + if (e.type == "page-count") { + let { totalPages } = e.detail; + // This means we have already handled the page count event + // and do not need to dispatch another event. + if (this._numPages == totalPages) { + return; + } + + this._numPages = totalPages; + this._rangeInput.disabled = false; + + let prevPages = Array.from(this._pagesSet); + this.updatePageRange(); + if ( + prevPages.length != this._pagesSet.size || + !prevPages.every(page => this._pagesSet.has(page)) + ) { + // If the calculated set of pages has changed then we need to dispatch + // a new pageRanges setting :( + // Ideally this would be resolved in the settings code since it should + // only happen for the "N-" case where pages N through the end of the + // document are in the range. + this.dispatchPageRange(false); + } + + return; + } + + if (e.target == this._rangePicker) { + this._rangeInput.hidden = e.target.value == "all"; + this.updatePageRange(); + this.dispatchPageRange(); + } else if (e.target == this._rangeInput) { + this._rangeInput.focus(); + if (this._numPages) { + this.updatePageRange(); + this.dispatchPageRange(); + } + } + } +} +customElements.define("page-range-input", PageRangeInput); + +class MarginsPicker extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + + this._marginPicker = this.querySelector("#margins-picker"); + this._customTopMargin = this.querySelector("#custom-margin-top"); + this._customBottomMargin = this.querySelector("#custom-margin-bottom"); + this._customLeftMargin = this.querySelector("#custom-margin-left"); + this._customRightMargin = this.querySelector("#custom-margin-right"); + this._marginError = this.querySelector("#error-invalid-margin"); + } + + get templateId() { + return "margins-template"; + } + + updateCustomMargins() { + let newMargins = { + marginTop: this._customTopMargin.value, + marginBottom: this._customBottomMargin.value, + marginLeft: this._customLeftMargin.value, + marginRight: this._customRightMargin.value, + }; + + this.dispatchSettingsChange({ + margins: "custom", + customMargins: newMargins, + }); + this._marginError.hidden = true; + } + + updateMaxValues() { + this._customTopMargin.max = + this._maxHeight - this._customBottomMargin.value; + this._customBottomMargin.max = + this._maxHeight - this._customTopMargin.value; + this._customLeftMargin.max = this._maxWidth - this._customRightMargin.value; + this._customRightMargin.max = this._maxWidth - this._customLeftMargin.value; + } + + formatMargin(target) { + if (target.value.includes(".")) { + if (target.value.split(".")[1].length > 2) { + let dotIndex = target.value.indexOf("."); + target.value = target.value.slice(0, dotIndex + 3); + } + } + } + + setAllMarginValues(settings) { + this._customTopMargin.value = parseFloat( + settings.customMargins.marginTop + ).toFixed(2); + this._customBottomMargin.value = parseFloat( + settings.customMargins.marginBottom + ).toFixed(2); + this._customLeftMargin.value = parseFloat( + settings.customMargins.marginLeft + ).toFixed(2); + this._customRightMargin.value = parseFloat( + settings.customMargins.marginRight + ).toFixed(2); + } + + update(settings) { + // Re-evaluate which margin options should be enabled whenever the printer or paper changes + if ( + settings.paperId !== this._paperId || + settings.printerName !== this._printerName || + settings.orientation !== this._orientation + ) { + let enabledMargins = settings.marginOptions; + for (let option of this._marginPicker.options) { + option.hidden = !enabledMargins[option.value]; + } + this._paperId = settings.paperId; + this._printerName = settings.printerName; + this._orientation = settings.orientation; + + let height = + this._orientation == 0 ? settings.paperHeight : settings.paperWidth; + let width = + this._orientation == 0 ? settings.paperWidth : settings.paperHeight; + + this._maxHeight = + height - + settings.unwriteableMarginTop - + settings.unwriteableMarginBottom; + this._maxWidth = + width - + settings.unwriteableMarginLeft - + settings.unwriteableMarginRight; + + this._defaultPresets = settings.defaultMargins; + // The values in custom fields should be initialized to custom margin values + // and must be overriden if they are no longer valid. + this.setAllMarginValues(settings); + this.updateMaxValues(); + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + this._marginError.hidden = true; + } + + // We need to ensure we don't override the value if the value should be custom. + if (this._marginPicker.value != "custom") { + // Reset the custom margin values if they are not valid and revalidate the form + if ( + !this._customTopMargin.checkValidity() || + !this._customBottomMargin.checkValidity() || + !this._customLeftMargin.checkValidity() || + !this._customRightMargin.checkValidity() + ) { + window.clearTimeout(this.showErrorTimeoutId); + this.setAllMarginValues(settings); + this.updateMaxValues(); + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + this._marginError.hidden = true; + } + if (settings.margins == "custom") { + // Ensure that we display the custom margin boxes + this.querySelector(".margin-group").hidden = false; + } + this._marginPicker.value = settings.margins; + } + } + + handleEvent(e) { + if (e.target == this._marginPicker) { + let customMargin = e.target.value == "custom"; + this.querySelector(".margin-group").hidden = !customMargin; + if (customMargin) { + // Update the custom margin values to ensure consistency + this.updateCustomMargins(); + return; + } + + this.dispatchSettingsChange({ + margins: e.target.value, + customMargins: null, + }); + } + + if ( + e.target == this._customTopMargin || + e.target == this._customBottomMargin || + e.target == this._customLeftMargin || + e.target == this._customRightMargin + ) { + if (e.target.checkValidity()) { + this.updateMaxValues(); + } + if ( + this._customTopMargin.validity.valid && + this._customBottomMargin.validity.valid && + this._customLeftMargin.validity.valid && + this._customRightMargin.validity.valid + ) { + this.formatMargin(e.target); + this.updateCustomMargins(); + } else if (e.target.validity.stepMismatch) { + // If this is the third digit after the decimal point, we should + // truncate the string. + this.formatMargin(e.target); + } + } + + window.clearTimeout(this.showErrorTimeoutId); + if ( + this._customTopMargin.validity.valid && + this._customBottomMargin.validity.valid && + this._customLeftMargin.validity.valid && + this._customRightMargin.validity.valid + ) { + this._marginError.hidden = true; + } else { + this.cancelSettingsChange({ customMargins: true, margins: true }); + this.showErrorTimeoutId = window.setTimeout(() => { + this._marginError.hidden = false; + }, INPUT_DELAY_MS); + } + } +} +customElements.define("margins-select", MarginsPicker); + +class TwistySummary extends PrintUIControlMixin(HTMLElement) { + get isOpen() { + return this.closest("details")?.hasAttribute("open"); + } + + get templateId() { + return "twisty-summary-template"; + } + + initialize() { + if (this._initialized) { + return; + } + super.initialize(); + this.label = this.querySelector(".label"); + + this.addEventListener("click", this); + this.updateSummary(); + } + + handleEvent(e) { + let willOpen = !this.isOpen; + this.updateSummary(willOpen); + } + + updateSummary(open = false) { + document.l10n.setAttributes( + this.label, + open + ? this.getAttribute("data-open-l10n-id") + : this.getAttribute("data-closed-l10n-id") + ); + } +} +customElements.define("twisty-summary", TwistySummary); + +class PageCount extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + document.addEventListener("page-count", this); + } + + update(settings) { + this.numCopies = settings.numCopies; + this.render(); + } + + render() { + if (!this.numCopies || !this.sheetCount) { + return; + } + document.l10n.setAttributes(this, "printui-sheets-count", { + sheetCount: this.sheetCount * this.numCopies, + }); + + // The loading attribute must be removed on first render + if (this.hasAttribute("loading")) { + this.removeAttribute("loading"); + } + + if (this.id) { + // We're showing the sheet count, so let it describe the dialog. + document.body.setAttribute("aria-describedby", this.id); + } + } + + handleEvent(e) { + this.sheetCount = e.detail.sheetCount; + this.render(); + } +} +customElements.define("page-count", PageCount); + +class PrintButton extends PrintUIControlMixin(HTMLButtonElement) { + update(settings) { + let l10nId = + settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? "printui-primary-button-save" + : "printui-primary-button"; + document.l10n.setAttributes(this, l10nId); + } +} +customElements.define("print-button", PrintButton, { extends: "button" }); + +class CancelButton extends HTMLButtonElement { + constructor() { + super(); + this.addEventListener("click", () => { + this.dispatchEvent(new Event("cancel-print", { bubbles: true })); + }); + } +} +customElements.define("cancel-button", CancelButton, { extends: "button" }); + +async function pickFileName(contentTitle, currentURI) { + let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [title] = await document.l10n.formatMessages([ + { id: "printui-save-to-pdf-title" }, + ]); + title = title.value; + + let filename; + if (contentTitle != "") { + filename = contentTitle; + } else { + let url = new URL(currentURI); + let path = decodeURIComponent(url.pathname); + path = path.replace(/\/$/, ""); + filename = path.split("/").pop(); + if (filename == "") { + filename = url.hostname; + } + } + if (!filename.endsWith(".pdf")) { + // macOS and linux don't set the extension based on the default extension. + // Windows won't add the extension a second time, fortunately. + // If it already ends with .pdf though, adding it again isn't needed. + filename += ".pdf"; + } + filename = DownloadPaths.sanitize(filename); + + picker.init( + window.docShell.chromeEventHandler.ownerGlobal, + title, + Ci.nsIFilePicker.modeSave + ); + picker.appendFilter("PDF", "*.pdf"); + picker.defaultExtension = "pdf"; + picker.defaultString = filename; + + let retval = await new Promise(resolve => picker.open(resolve)); + + if (retval == 1) { + throw new Error({ reason: "cancelled" }); + } else { + // OK clicked (retval == 0) or replace confirmed (retval == 2) + + // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file), + // the print progress listener is never called. This workaround ensures that a correct status is always returned. + try { + let fstream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw- + fstream.close(); + + // Remove the file to reduce the likelihood of the user opening an empty or damaged fle when the + // preview is loading + await IOUtils.remove(picker.file.path); + } catch (e) { + throw new Error({ reason: retval == 0 ? "not_saved" : "not_replaced" }); + } + } + + return picker.file.path; +} diff --git a/toolkit/components/printing/content/printPageSetup.js b/toolkit/components/printing/content/printPageSetup.js new file mode 100644 index 0000000000..ed02a45af1 --- /dev/null +++ b/toolkit/components/printing/content/printPageSetup.js @@ -0,0 +1,540 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var gDialog; +var paramBlock; +var gPrintService = null; +var gPrintSettings = null; +var gStringBundle = null; +var gDoingMetric = false; + +var gPrintSettingsInterface = Ci.nsIPrintSettings; +var gDoDebug = false; + +// --------------------------------------------------- +function initDialog() { + gDialog = {}; + + gDialog.orientation = document.getElementById("orientation"); + gDialog.portrait = document.getElementById("portrait"); + gDialog.landscape = document.getElementById("landscape"); + + gDialog.printBG = document.getElementById("printBG"); + + gDialog.shrinkToFit = document.getElementById("shrinkToFit"); + + gDialog.marginGroup = document.getElementById("marginGroup"); + + gDialog.marginPage = document.getElementById("marginPage"); + gDialog.marginTop = document.getElementById("marginTop"); + gDialog.marginBottom = document.getElementById("marginBottom"); + gDialog.marginLeft = document.getElementById("marginLeft"); + gDialog.marginRight = document.getElementById("marginRight"); + + gDialog.topInput = document.getElementById("topInput"); + gDialog.bottomInput = document.getElementById("bottomInput"); + gDialog.leftInput = document.getElementById("leftInput"); + gDialog.rightInput = document.getElementById("rightInput"); + + gDialog.hLeftOption = document.getElementById("hLeftOption"); + gDialog.hCenterOption = document.getElementById("hCenterOption"); + gDialog.hRightOption = document.getElementById("hRightOption"); + + gDialog.fLeftOption = document.getElementById("fLeftOption"); + gDialog.fCenterOption = document.getElementById("fCenterOption"); + gDialog.fRightOption = document.getElementById("fRightOption"); + + gDialog.scalingLabel = document.getElementById("scalingInput"); + gDialog.scalingInput = document.getElementById("scalingInput"); + + gDialog.enabled = false; + + document.addEventListener("dialogaccept", onAccept); +} + +// --------------------------------------------------- +function isListOfPrinterFeaturesAvailable() { + return Services.prefs.getBoolPref( + "print.tmp.printerfeatures." + + gPrintSettings.printerName + + ".has_special_printerfeatures", + false + ); +} + +// --------------------------------------------------- +function checkDouble(element) { + element.value = element.value.replace(/[^.0-9]/g, ""); +} + +// Theoretical paper width/height. +var gPageWidth = 8.5; +var gPageHeight = 11.0; + +// --------------------------------------------------- +function setOrientation() { + var selection = gDialog.orientation.selectedItem; + + var style = "background-color:white;"; + if ( + (selection == gDialog.portrait && gPageWidth > gPageHeight) || + (selection == gDialog.landscape && gPageWidth < gPageHeight) + ) { + // Swap width/height. + var temp = gPageHeight; + gPageHeight = gPageWidth; + gPageWidth = temp; + } + var div = gDoingMetric ? 100 : 10; + style += + "width:" + + gPageWidth / div + + unitString() + + ";height:" + + gPageHeight / div + + unitString() + + ";"; + gDialog.marginPage.setAttribute("style", style); +} + +// --------------------------------------------------- +function unitString() { + return gPrintSettings.paperSizeUnit == + gPrintSettingsInterface.kPaperSizeInches + ? "in" + : "mm"; +} + +// --------------------------------------------------- +function checkMargin(value, max, other) { + // Don't draw this margin bigger than permitted. + return Math.min(value, max - other.value); +} + +// --------------------------------------------------- +function changeMargin(node) { + // Correct invalid input. + checkDouble(node); + + // Reset the margin height/width for this node. + var val = node.value; + var nodeToStyle; + var attr = "width"; + if (node == gDialog.topInput) { + nodeToStyle = gDialog.marginTop; + val = checkMargin(val, gPageHeight, gDialog.bottomInput); + attr = "height"; + } else if (node == gDialog.bottomInput) { + nodeToStyle = gDialog.marginBottom; + val = checkMargin(val, gPageHeight, gDialog.topInput); + attr = "height"; + } else if (node == gDialog.leftInput) { + nodeToStyle = gDialog.marginLeft; + val = checkMargin(val, gPageWidth, gDialog.rightInput); + } else { + nodeToStyle = gDialog.marginRight; + val = checkMargin(val, gPageWidth, gDialog.leftInput); + } + var style = attr + ":" + val / 10 + unitString() + ";"; + nodeToStyle.setAttribute("style", style); +} + +// --------------------------------------------------- +function changeMargins() { + changeMargin(gDialog.topInput); + changeMargin(gDialog.bottomInput); + changeMargin(gDialog.leftInput); + changeMargin(gDialog.rightInput); +} + +// --------------------------------------------------- +async function customize(node) { + // If selection is now "Custom..." then prompt user for custom setting. + if (node.value == 6) { + let [title, promptText] = await document.l10n.formatValues([ + { id: "custom-prompt-title" }, + { id: "custom-prompt-prompt" }, + ]); + var result = { value: node.custom }; + var ok = Services.prompt.prompt(window, title, promptText, result, null, { + value: false, + }); + if (ok) { + node.custom = result.value; + } + } +} + +// --------------------------------------------------- +function setHeaderFooter(node, value) { + node.value = hfValueToId(value); + if (node.value == 6) { + // Remember current Custom... value. + node.custom = value; + } else { + // Start with empty Custom... value. + node.custom = ""; + } +} + +var gHFValues = []; +gHFValues["&T"] = 1; +gHFValues["&U"] = 2; +gHFValues["&D"] = 3; +gHFValues["&P"] = 4; +gHFValues["&PT"] = 5; + +function hfValueToId(val) { + if (val in gHFValues) { + return gHFValues[val]; + } + if (val.length) { + return 6; // Custom... + } + return 0; // --blank-- +} + +function hfIdToValue(node) { + var result = ""; + switch (parseInt(node.value)) { + case 0: + break; + case 1: + result = "&T"; + break; + case 2: + result = "&U"; + break; + case 3: + result = "&D"; + break; + case 4: + result = "&P"; + break; + case 5: + result = "&PT"; + break; + case 6: + result = node.custom; + break; + } + return result; +} + +async function lastUsedPrinterNameOrDefault() { + let printerList = Cc["@mozilla.org/gfx/printerlist;1"].getService( + Ci.nsIPrinterList + ); + let lastUsedName = gPrintService.lastUsedPrinterName; + let printers = await printerList.printers; + for (let printer of printers) { + printer.QueryInterface(Ci.nsIPrinter); + if (printer.name == lastUsedName) { + return lastUsedName; + } + } + return printerList.systemDefaultPrinterName; +} + +async function setPrinterDefaultsForSelectedPrinter() { + if (gPrintSettings.printerName == "") { + gPrintSettings.printerName = await lastUsedPrinterNameOrDefault(); + } + + // First get any defaults from the printer + gPrintService.initPrintSettingsFromPrinter( + gPrintSettings.printerName, + gPrintSettings + ); + + // now augment them with any values from last time + gPrintService.initPrintSettingsFromPrefs( + gPrintSettings, + true, + gPrintSettingsInterface.kInitSaveAll + ); + + if (gDoDebug) { + dump( + "pagesetup/setPrinterDefaultsForSelectedPrinter: printerName='" + + gPrintSettings.printerName + + "', orientation='" + + gPrintSettings.orientation + + "'\n" + ); + } +} + +// --------------------------------------------------- +async function loadDialog() { + var print_orientation = 0; + var print_margin_top = 0.5; + var print_margin_left = 0.5; + var print_margin_bottom = 0.5; + var print_margin_right = 0.5; + + try { + gPrintService = Cc["@mozilla.org/gfx/printsettings-service;1"]; + if (gPrintService) { + gPrintService = gPrintService.getService(); + if (gPrintService) { + gPrintService = gPrintService.QueryInterface( + Ci.nsIPrintSettingsService + ); + } + } + } catch (ex) { + dump("loadDialog: ex=" + ex + "\n"); + } + + await setPrinterDefaultsForSelectedPrinter(); + + gDialog.printBG.checked = + gPrintSettings.printBGColors || gPrintSettings.printBGImages; + + gDialog.shrinkToFit.checked = gPrintSettings.shrinkToFit; + + gDialog.scalingLabel.disabled = gDialog.scalingInput.disabled = + gDialog.shrinkToFit.checked; + + if ( + gPrintSettings.paperSizeUnit == gPrintSettingsInterface.kPaperSizeInches + ) { + document.l10n.setAttributes( + gDialog.marginGroup, + "margin-group-label-inches" + ); + gDoingMetric = false; + } else { + document.l10n.setAttributes( + gDialog.marginGroup, + "margin-group-label-metric" + ); + // Also, set global page dimensions for A4 paper, in millimeters (assumes portrait at this point). + gPageWidth = 2100; + gPageHeight = 2970; + gDoingMetric = true; + } + + print_orientation = gPrintSettings.orientation; + print_margin_top = convertMarginInchesToUnits( + gPrintSettings.marginTop, + gDoingMetric + ); + print_margin_left = convertMarginInchesToUnits( + gPrintSettings.marginLeft, + gDoingMetric + ); + print_margin_right = convertMarginInchesToUnits( + gPrintSettings.marginRight, + gDoingMetric + ); + print_margin_bottom = convertMarginInchesToUnits( + gPrintSettings.marginBottom, + gDoingMetric + ); + + if (gDoDebug) { + dump("print_orientation " + print_orientation + "\n"); + + dump("print_margin_top " + print_margin_top + "\n"); + dump("print_margin_left " + print_margin_left + "\n"); + dump("print_margin_right " + print_margin_right + "\n"); + dump("print_margin_bottom " + print_margin_bottom + "\n"); + } + + if (print_orientation == gPrintSettingsInterface.kPortraitOrientation) { + gDialog.orientation.selectedItem = gDialog.portrait; + } else if ( + print_orientation == gPrintSettingsInterface.kLandscapeOrientation + ) { + gDialog.orientation.selectedItem = gDialog.landscape; + } + + // Set orientation the first time on a timeout so the dialog sizes to the + // maximum height specified in the .xul file. Otherwise, if the user switches + // from landscape to portrait, the content grows and the buttons are clipped. + setTimeout(setOrientation, 0); + + gDialog.topInput.value = print_margin_top.toFixed(1); + gDialog.bottomInput.value = print_margin_bottom.toFixed(1); + gDialog.leftInput.value = print_margin_left.toFixed(1); + gDialog.rightInput.value = print_margin_right.toFixed(1); + changeMargins(); + + setHeaderFooter(gDialog.hLeftOption, gPrintSettings.headerStrLeft); + setHeaderFooter(gDialog.hCenterOption, gPrintSettings.headerStrCenter); + setHeaderFooter(gDialog.hRightOption, gPrintSettings.headerStrRight); + + setHeaderFooter(gDialog.fLeftOption, gPrintSettings.footerStrLeft); + setHeaderFooter(gDialog.fCenterOption, gPrintSettings.footerStrCenter); + setHeaderFooter(gDialog.fRightOption, gPrintSettings.footerStrRight); + + gDialog.scalingInput.value = (gPrintSettings.scaling * 100).toFixed(0); + + // Enable/disable widgets based in the information whether the selected + // printer supports the matching feature or not + if (isListOfPrinterFeaturesAvailable()) { + if ( + Services.prefs.getBoolPref( + "print.tmp.printerfeatures." + + gPrintSettings.printerName + + ".can_change_orientation" + ) + ) { + gDialog.orientation.removeAttribute("disabled"); + } else { + gDialog.orientation.setAttribute("disabled", "true"); + } + } + + // Give initial focus to the orientation radio group. + // Done on a timeout due to to bug 103197. + setTimeout(function() { + gDialog.orientation.focus(); + }, 0); +} + +// --------------------------------------------------- +function onLoad() { + // Init gDialog. + initDialog(); + + if (window.arguments[0] != null) { + gPrintSettings = window.arguments[0].QueryInterface(Ci.nsIPrintSettings); + paramBlock = window.arguments[1].QueryInterface(Ci.nsIDialogParamBlock); + } else if (gDoDebug) { + alert("window.arguments[0] == null!"); + } + + // default return value is "cancel" + paramBlock.SetInt(0, 0); + + if (gPrintSettings) { + loadDialog(); + } else if (gDoDebug) { + alert("Could initialize gDialog, PrintSettings is null!"); + } +} + +function convertUnitsMarginToInches(aVal, aIsMetric) { + if (aIsMetric) { + return aVal / 25.4; + } + return aVal; +} + +function convertMarginInchesToUnits(aVal, aIsMetric) { + if (aIsMetric) { + return aVal * 25.4; + } + return aVal; +} + +// --------------------------------------------------- +function onAccept() { + if (gPrintSettings) { + if (gDialog.orientation.selectedItem == gDialog.portrait) { + gPrintSettings.orientation = gPrintSettingsInterface.kPortraitOrientation; + } else { + gPrintSettings.orientation = + gPrintSettingsInterface.kLandscapeOrientation; + } + + // save these out so they can be picked up by the device spec + gPrintSettings.marginTop = convertUnitsMarginToInches( + gDialog.topInput.value, + gDoingMetric + ); + gPrintSettings.marginLeft = convertUnitsMarginToInches( + gDialog.leftInput.value, + gDoingMetric + ); + gPrintSettings.marginBottom = convertUnitsMarginToInches( + gDialog.bottomInput.value, + gDoingMetric + ); + gPrintSettings.marginRight = convertUnitsMarginToInches( + gDialog.rightInput.value, + gDoingMetric + ); + + gPrintSettings.headerStrLeft = hfIdToValue(gDialog.hLeftOption); + gPrintSettings.headerStrCenter = hfIdToValue(gDialog.hCenterOption); + gPrintSettings.headerStrRight = hfIdToValue(gDialog.hRightOption); + + gPrintSettings.footerStrLeft = hfIdToValue(gDialog.fLeftOption); + gPrintSettings.footerStrCenter = hfIdToValue(gDialog.fCenterOption); + gPrintSettings.footerStrRight = hfIdToValue(gDialog.fRightOption); + + gPrintSettings.printBGColors = gDialog.printBG.checked; + gPrintSettings.printBGImages = gDialog.printBG.checked; + + gPrintSettings.shrinkToFit = gDialog.shrinkToFit.checked; + + var scaling = document.getElementById("scalingInput").value; + if (scaling < 10.0) { + scaling = 10.0; + } + if (scaling > 500.0) { + scaling = 500.0; + } + scaling /= 100.0; + gPrintSettings.scaling = scaling; + + if (gDoDebug) { + dump("******* Page Setup Accepting ******\n"); + dump("print_margin_top " + gDialog.topInput.value + "\n"); + dump("print_margin_left " + gDialog.leftInput.value + "\n"); + dump("print_margin_right " + gDialog.bottomInput.value + "\n"); + dump("print_margin_bottom " + gDialog.rightInput.value + "\n"); + } + } + + // set return value to "ok" + if (paramBlock) { + paramBlock.SetInt(0, 1); + } else { + dump("*** FATAL ERROR: No paramBlock\n"); + } + + var flags = + gPrintSettingsInterface.kInitSaveMargins | + gPrintSettingsInterface.kInitSaveHeaderLeft | + gPrintSettingsInterface.kInitSaveHeaderCenter | + gPrintSettingsInterface.kInitSaveHeaderRight | + gPrintSettingsInterface.kInitSaveFooterLeft | + gPrintSettingsInterface.kInitSaveFooterCenter | + gPrintSettingsInterface.kInitSaveFooterRight | + gPrintSettingsInterface.kInitSaveBGColors | + gPrintSettingsInterface.kInitSaveBGImages | + gPrintSettingsInterface.kInitSaveInColor | + gPrintSettingsInterface.kInitSaveReversed | + gPrintSettingsInterface.kInitSaveOrientation | + gPrintSettingsInterface.kInitSaveShrinkToFit | + gPrintSettingsInterface.kInitSaveScaling; + + // Note that this file is Windows only code, so this doesn't handle saving + // for other platforms. + // XXX Should we do this in nsPrintDialogServiceWin::ShowPageSetup (the code + // that invokes us), since ShowPageSetup is where we do the saving for the + // other platforms? + gPrintService.savePrintSettingsToPrefs(gPrintSettings, true, flags); +} + +// --------------------------------------------------- +function onCancel() { + // set return value to "cancel" + if (paramBlock) { + paramBlock.SetInt(0, 0); + } else { + dump("*** FATAL ERROR: No paramBlock\n"); + } + + return true; +} diff --git a/toolkit/components/printing/content/printPageSetup.xhtml b/toolkit/components/printing/content/printPageSetup.xhtml new file mode 100644 index 0000000000..f1c7942a3b --- /dev/null +++ b/toolkit/components/printing/content/printPageSetup.xhtml @@ -0,0 +1,212 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML -*- --> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://global/skin/printPageSetup.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();" + oncancel="return onCancel();" + data-l10n-id="print-setup" + persist="screenX screenY" + screenX="24" screenY="24"> +<dialog id="printPageSetupDialog"> + + <linkset> + <html:link rel="localization" href="toolkit/printing/printDialogs.ftl"/> + </linkset> + + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + <script src="chrome://global/content/printPageSetup.js"/> + + <tabbox flex="1"> + <tabs> + <tab data-l10n-id="basic-tab"/> + <tab data-l10n-id="advanced-tab"/> + </tabs> + <tabpanels flex="1"> + <vbox> + <html:fieldset> + <html:legend><label data-l10n-id="format-group-label"/></html:legend> + <vbox class="groupbox-body"> + <hbox align="center"> + <label control="orientation" data-l10n-id="orientation-label"/> + <radiogroup id="orientation" oncommand="setOrientation()"> + <hbox align="center"> + <radio id="portrait" + class="portrait-page" + data-l10n-id="portrait"/> + <radio id="landscape" + class="landscape-page" + data-l10n-id="landscape"/> + </hbox> + </radiogroup> + </hbox> + <separator/> + <hbox align="center"> + <label control="scalingInput" + data-l10n-id="scale"/> + <html:input id="scalingInput" size="4" oninput="checkDouble(this)"/> + <label data-l10n-id="scale-percent"/> + <separator/> + <checkbox id="shrinkToFit" + data-l10n-id="shrink-to-fit" + oncommand="gDialog.scalingInput.disabled = gDialog.scalingLabel.disabled = this.checked"/> + </hbox> + </vbox> + </html:fieldset> + <html:fieldset> + <html:legend><label data-l10n-id="options-group-label"/></html:legend> + <checkbox id="printBG" + class="groupbox-body" + data-l10n-id="print-bg"/> + </html:fieldset> + </vbox> + <vbox> + <html:fieldset> + <html:legend><label id="marginGroup" data-l10n-id="margin-group-label"/></html:legend> + <vbox class="groupbox-body"> + <hbox align="center"> + <spacer flex="1"/> + <label control="topInput" + data-l10n-id="margin-top"/> + <html:input id="topInput" size="5" oninput="changeMargin(this)"/> + <!-- This invisible label (with same content as the visible one!) is used + to ensure that the <input> is centered above the page. The same + technique is deployed for the bottom/left/right input fields, below. --> + <label data-l10n-id="margin-top-invisible" style="visibility: hidden;"/> + <spacer flex="1"/> + </hbox> + <hbox dir="ltr"> + <spacer flex="1"/> + <vbox> + <spacer flex="1"/> + <label control="leftInput" + data-l10n-id="margin-left"/> + <html:input id="leftInput" size="5" oninput="changeMargin(this)"/> + <label data-l10n-id="margin-left-invisible" style="visibility: hidden;"/> + <spacer flex="1"/> + </vbox> + <!-- The "margin page" draws a simulated printout page with dashed lines + for the margins. The height/width style attributes of the marginTop, + marginBottom, marginLeft, and marginRight elements are set by + the JS code dynamically based on the user input. --> + <vbox id="marginPage" style="height:29.7mm;"> + <box id="marginTop" style="height:0.05in;"/> + <hbox flex="1" dir="ltr"> + <box id="marginLeft" style="width:0.025in;"/> + <box style="border: 1px; border-style: dashed; border-color: gray;" flex="1"/> + <box id="marginRight" style="width:0.025in;"/> + </hbox> + <box id="marginBottom" style="height:0.05in;"/> + </vbox> + <vbox> + <spacer flex="1"/> + <label control="rightInput" + data-l10n-id="margin-right"/> + <html:input id="rightInput" size="5" oninput="changeMargin(this)"/> + <label data-l10n-id="margin-right-invisible" style="visibility: hidden;"/> + <spacer flex="1"/> + </vbox> + <spacer flex="1"/> + </hbox> + <hbox align="center"> + <spacer flex="1"/> + <label control="bottomInput" + data-l10n-id="margin-bottom"/> + <html:input id="bottomInput" size="5" oninput="changeMargin(this)"/> + <label data-l10n-id="margin-bottom-invisible" style="visibility: hidden;"/> + <spacer flex="1"/> + </hbox> + </vbox> + </html:fieldset> + <html:fieldset> + <html:legend><label data-l10n-id="header-footer-label"/></html:legend> + <box id="header-footer-grid" class="groupbox-body" dir="ltr"> + <menulist id="hLeftOption" oncommand="customize(this)" data-l10n-id="header-left-tip"> + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank"/> + <menuitem value="1" data-l10n-id="hf-title"/> + <menuitem value="2" data-l10n-id="hf-url"/> + <menuitem value="3" data-l10n-id="hf-date-and-time"/> + <menuitem value="4" data-l10n-id="hf-page"/> + <menuitem value="5" data-l10n-id="hf-page-and-total"/> + <menuitem value="6" data-l10n-id="hf-custom"/> + </menupopup> + </menulist> + <menulist id="hCenterOption" oncommand="customize(this)" data-l10n-id="header-center-tip"> + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank"/> + <menuitem value="1" data-l10n-id="hf-title"/> + <menuitem value="2" data-l10n-id="hf-url"/> + <menuitem value="3" data-l10n-id="hf-date-and-time"/> + <menuitem value="4" data-l10n-id="hf-page"/> + <menuitem value="5" data-l10n-id="hf-page-and-total"/> + <menuitem value="6" data-l10n-id="hf-custom"/> + </menupopup> + </menulist> + <menulist id="hRightOption" oncommand="customize(this)" data-l10n-id="header-right-tip"> + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank"/> + <menuitem value="1" data-l10n-id="hf-title"/> + <menuitem value="2" data-l10n-id="hf-url"/> + <menuitem value="3" data-l10n-id="hf-date-and-time"/> + <menuitem value="4" data-l10n-id="hf-page"/> + <menuitem value="5" data-l10n-id="hf-page-and-total"/> + <menuitem value="6" data-l10n-id="hf-custom"/> + </menupopup> + </menulist> + <vbox align="center"> + <label data-l10n-id="hf-left-label"/> + </vbox> + <vbox align="center"> + <label data-l10n-id="hf-center-label"/> + </vbox> + <vbox align="center"> + <label data-l10n-id="hf-right-label"/> + </vbox> + <menulist id="fLeftOption" oncommand="customize(this)" data-l10n-id="footer-left-tip"> + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank"/> + <menuitem value="1" data-l10n-id="hf-title"/> + <menuitem value="2" data-l10n-id="hf-url"/> + <menuitem value="3" data-l10n-id="hf-date-and-time"/> + <menuitem value="4" data-l10n-id="hf-page"/> + <menuitem value="5" data-l10n-id="hf-page-and-total"/> + <menuitem value="6" data-l10n-id="hf-custom"/> + </menupopup> + </menulist> + <menulist id="fCenterOption" oncommand="customize(this)" data-l10n-id="footer-center-tip"> + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank"/> + <menuitem value="1" data-l10n-id="hf-title"/> + <menuitem value="2" data-l10n-id="hf-url"/> + <menuitem value="3" data-l10n-id="hf-date-and-time"/> + <menuitem value="4" data-l10n-id="hf-page"/> + <menuitem value="5" data-l10n-id="hf-page-and-total"/> + <menuitem value="6" data-l10n-id="hf-custom"/> + </menupopup> + </menulist> + <menulist id="fRightOption" oncommand="customize(this)" data-l10n-id="footer-right-tip"> + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank"/> + <menuitem value="1" data-l10n-id="hf-title"/> + <menuitem value="2" data-l10n-id="hf-url"/> + <menuitem value="3" data-l10n-id="hf-date-and-time"/> + <menuitem value="4" data-l10n-id="hf-page"/> + <menuitem value="5" data-l10n-id="hf-page-and-total"/> + <menuitem value="6" data-l10n-id="hf-custom"/> + </menupopup> + </menulist> + </box> + </html:fieldset> + </vbox> + </tabpanels> + </tabbox> +</dialog> +</window> diff --git a/toolkit/components/printing/content/printPagination.css b/toolkit/components/printing/content/printPagination.css new file mode 100644 index 0000000000..fd2e7883bc --- /dev/null +++ b/toolkit/components/printing/content/printPagination.css @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +:host { + /* in-content/common.css variables */ + --blue-50: #0a84ff; + --grey-90-a10: rgba(12, 12, 13, 0.1); + --shadow-30: 0 4px 16px var(--grey-90-a10); + --border-active-shadow: var(--blue-50); + --border-active-color: ButtonShadow; +} + +:host { + display: block; + position: absolute; + bottom: 24px; + inset-inline-start: 50%; + translate: -50%; +} +:host(:-moz-locale-dir(rtl)) { + translate: 50%; +} + +.container { + margin-inline: auto; + align-items: center; + display: flex; + justify-content: center; + box-shadow: var(--shadow-30); + color: var(--toolbar-color); + background-color: var(--toolbar-bgcolor); + border-radius: 6px; + border-style: none; +} + +.toolbarButton, +.toolbarCenter { + align-self: stretch; + flex: 0 0 auto; + padding: var(--toolbarbutton-outer-padding); + border: none; + border-inline-end: 1px solid ThreeDShadow; + border-block: 1px solid ThreeDShadow; + color: inherit; + background-color: transparent; + min-width: calc(2 * var(--toolbarbutton-inner-padding) + 16px); + min-height: calc(2 * var(--toolbarbutton-inner-padding) + 16px); +} +.startItem { + border-inline-start: 1px solid ThreeDShadow; + border-start-start-radius: 6px; + border-end-start-radius: 6px; +} +.endItem { + border-start-end-radius: 6px; + border-end-end-radius: 6px; +} + +.toolbarButton::after { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + vertical-align: text-bottom; + text-align: center; + background-repeat: no-repeat; + background-position: center center; + background-size: 12px; + -moz-context-properties: fill, fill-opacity; + fill: var(--lwt-toolbarbutton-icon-fill, currentColor); + fill-opacity: var(--toolbarbutton-icon-fill-opacity); +} + +.toolbarButton:hover { + background-color: var(--toolbarbutton-hover-background); +} +.toolbarButton:hover:active { + background-color: var(--toolbarbutton-active-background); +} +.toolbarButton::-moz-focus-inner { + border: none; +} +.toolbarButton:focus { + z-index: 1; +} + +.toolbarButton:-moz-focusring { + outline: 2px solid var(--border-active-shadow); +} +.toolbarButton.startItem:-moz-focusring, +.toolbarButton.endItem:-moz-locale-dir(rtl):-moz-focusring { + -moz-outline-radius: 8px; + -moz-outline-radius-topright: 0; + -moz-outline-radius-bottomright: 0; +} +.toolbarButton.endItem:-moz-focusring, +.toolbarButton.startItem:-moz-locale-dir(rtl):-moz-focusring { + -moz-outline-radius: 8px; + -moz-outline-radius-topleft: 0; + -moz-outline-radius-bottomleft: 0; +} + +.toolbarCenter { + flex-shrink: 0; + /* 3 chars + (3px border + 1px padding) on both sides */ + min-width: calc(8px + 3ch); + padding: 0 32px; + display: flex; + align-items: center; + justify-content: center; +} + +#navigateHome::after, +#navigateEnd::after { + background-image: url("chrome://global/skin/icons/chevron.svg"); +} + +#navigatePrevious::after, +#navigateNext::after { + background-image: url("chrome://global/skin/icons/arrow-left.svg"); +} + +#navigatePrevious:-moz-locale-dir(rtl)::after, +#navigateEnd:-moz-locale-dir(rtl)::after, +#navigateHome:-moz-locale-dir(ltr)::after, +#navigateNext:-moz-locale-dir(ltr)::after { + transform: scaleX(-1); +} + +/* progressively hide the navigation buttons when the print preview is too narrow to fit */ +@media (max-width: 550px) { + #navigatePrevious, + #navigateNext, + #navigateEnd, + #navigateHome { + display: none; + } + .toolbarCenter { + border-inline-start: 1px solid ThreeDShadow; + border-radius: 6px; + } +} diff --git a/toolkit/components/printing/content/printPreviewPagination.js b/toolkit/components/printing/content/printPreviewPagination.js new file mode 100644 index 0000000000..cd426f2f77 --- /dev/null +++ b/toolkit/components/printing/content/printPreviewPagination.js @@ -0,0 +1,208 @@ +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ + +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +customElements.define( + "printpreview-pagination", + class PrintPreviewPagination extends HTMLElement { + static get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/content/printPagination.css" /> + <html:div class="container"> + <html:button id="navigateHome" class="toolbarButton startItem" data-l10n-id="printpreview-homearrow-button"></html:button> + <html:button id="navigatePrevious" class="toolbarButton" data-l10n-id="printpreview-previousarrow-button"></html:button> + <html:div class="toolbarCenter"><html:span id="sheetIndicator" data-l10n-id="printpreview-sheet-of-sheets" data-l10n-args='{ "sheetNum": 1, "sheetCount": 1 }'></html:span></html:div> + <html:button id="navigateNext" class="toolbarButton" data-l10n-id="printpreview-nextarrow-button"></html:button> + <html:button id="navigateEnd" class="toolbarButton endItem" data-l10n-id="printpreview-endarrow-button"></html:button> + </html:div> + `; + } + + static get defaultProperties() { + return { + currentPage: 1, + sheetCount: 1, + }; + } + + get previewBrowser() { + if (!this._previewBrowser) { + // Assuming we're a sibling of our preview browser. + this._previewBrowser = this.parentNode.querySelector( + ".printPreviewBrowser" + ); + } + return this._previewBrowser; + } + + set previewBrowser(aBrowser) { + this._previewBrowser = aBrowser; + } + + connectedCallback() { + MozXULElement.insertFTLIfNeeded("toolkit/printing/printPreview.ftl"); + + const shadowRoot = this.attachShadow({ mode: "open" }); + document.l10n.connectRoot(shadowRoot); + + let fragment = MozXULElement.parseXULToFragment(this.constructor.markup); + this.shadowRoot.append(fragment); + + this.elements = { + sheetIndicator: shadowRoot.querySelector("#sheetIndicator"), + homeButton: shadowRoot.querySelector("#navigateHome"), + previousButton: shadowRoot.querySelector("#navigatePrevious"), + nextButton: shadowRoot.querySelector("#navigateNext"), + endButton: shadowRoot.querySelector("#navigateEnd"), + }; + + this.shadowRoot.addEventListener("click", this); + + let knownAttrs = { + "sheet-count": "sheetCount", + "current-page": "currentPage", + }; + this.mutationObserver = new MutationObserver(changes => { + let opts = {}; + for (let change of changes) { + let { attributeName, target, type } = change; + if (type == "attributes" && attributeName in knownAttrs) { + opts[knownAttrs[attributeName]] = parseInt( + target.getAttribute(attributeName), + 10 + ); + } + } + if (opts.sheetCount || opts.currentPage) { + this.update(opts); + } + }); + this.mutationObserver.observe(this.previewBrowser, { + attributes: ["current-page", "sheet-count"], + }); + + this.currentPreviewBrowserObserver = new MutationObserver(changes => { + for (let change of changes) { + if (change.attributeName == "previewtype") { + let previewType = change.target.getAttribute("previewtype"); + this.previewBrowser = change.target.querySelector( + `browser[previewtype="${previewType}"]` + ); + this.mutationObserver.disconnect(); + this.mutationObserver.observe(this.previewBrowser, { + attributes: ["current-page", "sheet-count"], + }); + } + } + }); + this.currentPreviewBrowserObserver.observe(this.parentNode, { + attributes: ["previewtype"], + }); + + // Initial render with some default values + // We'll be updated with real values when available + this.update(this.constructor.defaultProperties); + } + + disconnectedCallback() { + document.l10n.disconnectRoot(this.shadowRoot); + this.shadowRoot.textContent = ""; + this.mutationObserver?.disconnect(); + delete this.mutationObserver; + this.currentPreviewBrowserObserver?.disconnect(); + delete this.currentPreviewBrowserObserver; + } + + handleEvent(event) { + if (event.type == "click" && event.button != 0) { + return; + } + event.stopPropagation(); + + switch (event.target) { + case this.elements.homeButton: + this.navigate(0, 0, "home"); + break; + case this.elements.previousButton: + this.navigate(-1, 0, 0); + break; + case this.elements.nextButton: + this.navigate(1, 0, 0); + break; + case this.elements.endButton: + this.navigate(0, 0, "end"); + break; + } + } + + navigate(aDirection, aPageNum, aHomeOrEnd) { + const nsIWebBrowserPrint = Ci.nsIWebBrowserPrint; + let targetNum; + let navType; + // we use only one of aHomeOrEnd, aDirection, or aPageNum + if (aHomeOrEnd) { + // We're going to either the very first page ("home"), or the + // very last page ("end"). + if (aHomeOrEnd == "home") { + targetNum = 1; + navType = nsIWebBrowserPrint.PRINTPREVIEW_HOME; + } else { + targetNum = this.sheetCount; + navType = nsIWebBrowserPrint.PRINTPREVIEW_END; + } + } else if (aPageNum) { + // We're going to a specific page (aPageNum) + targetNum = Math.min(Math.max(1, aPageNum), this.sheetCount); + navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM; + } else { + // aDirection is either +1 or -1, and allows us to increment + // or decrement our currently viewed page. + targetNum = Math.min( + Math.max(1, this.currentSheet + aDirection), + this.sheetCount + ); + navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM; + } + + // Preemptively update our own state, rather than waiting for the message from the child process + // This allows subsequent clicks of next/back to advance 1 page per click if possible + // and keeps the UI feeling more responsive + this.update({ currentPage: targetNum }); + + this.previewBrowser.sendMessageToActor( + "Printing:Preview:Navigate", + { + navType, + pageNum: targetNum, + }, + "Printing" + ); + } + + update(data = {}) { + if (data.sheetCount) { + if (this.sheetCount !== data.sheetCount || this.currentSheet !== 1) { + // when sheet count changes, scroll position will get reset + this.currentSheet = 1; + } + this.sheetCount = data.sheetCount; + } + if (data.currentPage) { + this.currentSheet = data.currentPage; + } + document.l10n.setAttributes( + this.elements.sheetIndicator, + this.elements.sheetIndicator.dataset.l10nId, + { + sheetNum: this.currentSheet, + sheetCount: this.sheetCount, + } + ); + } + } +); diff --git a/toolkit/components/printing/content/printPreviewProgress.js b/toolkit/components/printing/content/printPreviewProgress.js new file mode 100644 index 0000000000..783f434a34 --- /dev/null +++ b/toolkit/components/printing/content/printPreviewProgress.js @@ -0,0 +1,152 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// dialog is just an array we'll use to store various properties from the dialog document... +var dialog; + +// the printProgress is a nsIPrintProgress object +var printProgress = null; + +// random global variables... +var targetFile; + +var docTitle = ""; +var docURL = ""; +var progressParams = null; + +function ellipseString(aStr, doFront) { + if (!aStr) { + return ""; + } + + if ( + aStr.length > 3 && + (aStr.substr(0, 3) == "..." || aStr.substr(aStr.length - 4, 3) == "...") + ) { + return aStr; + } + + var fixedLen = 64; + if (aStr.length <= fixedLen) { + return aStr; + } + + if (doFront) { + return "..." + aStr.substr(aStr.length - fixedLen, fixedLen); + } + + return aStr.substr(0, fixedLen) + "..."; +} + +// all progress notifications are done through the nsIWebProgressListener implementation... +var progressListener = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + window.close(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + if (!progressParams) { + return; + } + var docTitleStr = ellipseString(progressParams.docTitle, false); + if (docTitleStr != docTitle) { + docTitle = docTitleStr; + dialog.title.value = docTitle; + } + var docURLStr = ellipseString(progressParams.docURL, true); + if (docURLStr != docURL && dialog.title != null) { + docURL = docURLStr; + if (docTitle == "") { + dialog.title.value = docURLStr; + } + } + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onSecurityChange(aWebProgress, aRequest, state) {}, + onContentBlockingEvent(aWebProgress, aRequest, event) {}, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + if (aMessage) { + dialog.title.setAttribute("value", aMessage); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +function onLoad() { + // Set global variables. + printProgress = window.arguments[0]; + if (window.arguments[1]) { + progressParams = window.arguments[1].QueryInterface( + Ci.nsIPrintProgressParams + ); + if (progressParams) { + docTitle = ellipseString(progressParams.docTitle, false); + docURL = ellipseString(progressParams.docURL, true); + } + } + + if (!printProgress) { + dump("Invalid argument to printPreviewProgress.xhtml\n"); + window.close(); + return; + } + + dialog = {}; + dialog.strings = []; + dialog.title = document.getElementById("dialog.title"); + dialog.titleLabel = document.getElementById("dialog.titleLabel"); + + dialog.title.value = docTitle; + + // set our web progress listener on the helper app launcher + printProgress.registerListener(progressListener); + + // We need to delay the set title else dom will overwrite it + window.setTimeout(doneIniting, 100); +} + +function onUnload() { + if (!printProgress) { + return; + } + try { + printProgress.unregisterListener(progressListener); + printProgress = null; + } catch (e) {} +} + +// If the user presses cancel, tell the app launcher and close the dialog... +function onCancel() { + // Cancel app launcher. + try { + printProgress.processCanceledByUser = true; + } catch (e) { + return true; + } + + // don't Close up dialog by returning false, the backend will close the dialog when everything will be aborted. + return false; +} + +function doneIniting() { + // called by function timeout in onLoad + printProgress.doneIniting(); +} diff --git a/toolkit/components/printing/content/printPreviewProgress.xhtml b/toolkit/components/printing/content/printPreviewProgress.xhtml new file mode 100644 index 0000000000..02ab68b073 --- /dev/null +++ b/toolkit/components/printing/content/printPreviewProgress.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="print-preview-window" + style="width: 36em;" + oncancel="onCancel()" + onload="onLoad()" + onunload="onUnload()"> +<dialog buttons="cancel"> + + <linkset> + <html:link rel="localization" href="toolkit/printing/printDialogs.ftl"/> + </linkset> + + <script src="chrome://global/content/printPreviewProgress.js"/> + + <box flex="1" style="display: grid; grid-template-columns: min-content max-content; grid-template-rows: max-content max-content;"> + <!-- First row --> + <label id="dialog.titleLabel" data-l10n-id="print-title" style="justify-self: end;"/> + <label id="dialog.title"/> + + <!-- Second row --> + <label id="dialog.progressLabel" data-l10n-id="print-progress" style="justify-self: end;"/> + <label id="dialog.progressSpaces" data-l10n-id="print-preparing"/> + </box> +</dialog> +</window> diff --git a/toolkit/components/printing/content/printPreviewToolbar.js b/toolkit/components/printing/content/printPreviewToolbar.js new file mode 100644 index 0000000000..b2ff4e8259 --- /dev/null +++ b/toolkit/components/printing/content/printPreviewToolbar.js @@ -0,0 +1,444 @@ +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ + +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +customElements.define( + "printpreview-toolbar", + class PrintPreviewToolbar extends MozXULElement { + static get markup() { + return ` + <button id="print-preview-print" oncommand="this.parentNode.print();" data-l10n-id="printpreview-print"/> + <button id="print-preview-pageSetup" oncommand="this.parentNode.doPageSetup();" data-l10n-id="printpreview-page-setup"/> + <vbox align="center" pack="center"> + <label control="print-preview-pageNumber" data-l10n-id="printpreview-page"/> + </vbox> + <toolbarbutton id="print-preview-navigateHome" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(0, 0, 'home');" data-l10n-id="printpreview-homearrow"/> + <toolbarbutton id="print-preview-navigatePrevious" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(-1, 0, 0);" data-l10n-id="printpreview-previousarrow"/> + <hbox align="center" pack="center"> + <html:input id="print-preview-pageNumber" hidespinbuttons="true" type="number" value="1" min="1"/> + <label data-l10n-id="printpreview-of"/> + <label id="print-preview-totalPages" value="1"/> + </hbox> + <toolbarbutton id="print-preview-navigateNext" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(1, 0, 0);" data-l10n-id="printpreview-nextarrow"/> + <toolbarbutton id="print-preview-navigateEnd" class="print-preview-navigate-button tabbable" oncommand="parentNode.navigate(0, 0, 'end');" data-l10n-id="printpreview-endarrow"/> + <toolbarseparator class="toolbarseparator-primary"/> + <vbox align="center" pack="center"> + <label id="print-preview-scale-label" control="print-preview-scale" data-l10n-id="printpreview-scale"/> + </vbox> + <hbox align="center" pack="center"> + <menulist id="print-preview-scale" crop="none" oncommand="parentNode.parentNode.scale(this.selectedItem.value);"> + <menupopup> + <menuitem value="0.3" /> + <menuitem value="0.4" /> + <menuitem value="0.5" /> + <menuitem value="0.6" /> + <menuitem value="0.7" /> + <menuitem value="0.8" /> + <menuitem value="0.9" /> + <menuitem value="1" /> + <menuitem value="1.25" /> + <menuitem value="1.5" /> + <menuitem value="1.75" /> + <menuitem value="2" /> + <menuseparator/> + <menuitem flex="1" value="ShrinkToFit" data-l10n-id="printpreview-shrink-to-fit"/> + <menuitem value="Custom" data-l10n-id="printpreview-custom"/> + </menupopup> + </menulist> + </hbox> + <toolbarseparator class="toolbarseparator-primary"/> + <hbox align="center" pack="center"> + <toolbarbutton id="print-preview-portrait-button" checked="true" type="radio" group="orient" class="tabbable" oncommand="parentNode.parentNode.orient('portrait');" data-l10n-id="printpreview-portrait"/> + <toolbarbutton id="print-preview-landscape-button" type="radio" group="orient" class="tabbable" oncommand="parentNode.parentNode.orient('landscape');" data-l10n-id="printpreview-landscape"/> + </hbox> + <toolbarseparator class="toolbarseparator-primary"/> + <checkbox id="print-preview-simplify" checked="false" disabled="true" oncommand="this.parentNode.simplify();" data-l10n-id="printpreview-simplify-page-checkbox"/> + <toolbarseparator class="toolbarseparator-primary"/> + <button id="print-preview-toolbar-close-button" oncommand="PrintUtils.exitPrintPreview();" data-l10n-id="printpreview-close"/> + <data id="print-preview-custom-scale-prompt-title" data-l10n-id="printpreview-custom-scale-prompt-title"/> + `; + } + constructor() { + super(); + this.disconnectedCallback = this.disconnectedCallback.bind(this); + } + connectedCallback() { + window.addEventListener("unload", this.disconnectedCallback, { + once: true, + }); + + MozXULElement.insertFTLIfNeeded("toolkit/printing/printPreview.ftl"); + this.appendChild(this.constructor.fragment); + + this.mPrintButton = document.getElementById("print-preview-print"); + + this.mPageSetupButton = document.getElementById( + "print-preview-pageSetup" + ); + + this.mNavigateHomeButton = document.getElementById( + "print-preview-navigateHome" + ); + + this.mNavigatePreviousButton = document.getElementById( + "print-preview-navigatePrevious" + ); + + this.mPageTextBox = document.getElementById("print-preview-pageNumber"); + + this.mNavigateNextButton = document.getElementById( + "print-preview-navigateNext" + ); + + this.mNavigateEndButton = document.getElementById( + "print-preview-navigateEnd" + ); + + this.mTotalPages = document.getElementById("print-preview-totalPages"); + + this.mScaleCombobox = document.getElementById("print-preview-scale"); + + this.mPortaitButton = document.getElementById( + "print-preview-portrait-button" + ); + + this.mLandscapeButton = document.getElementById( + "print-preview-landscape-button" + ); + + this.mSimplifyPageCheckbox = document.getElementById( + "print-preview-simplify" + ); + + this.mSimplifyPageNotAllowed = this.mSimplifyPageCheckbox.disabled; + + this.mSimplifyPageToolbarSeparator = this.mSimplifyPageCheckbox.nextElementSibling; + + this.mPrintPreviewObs = ""; + + this.mWebProgress = ""; + + this.mPPBrowser = null; + + this.mOnPageTextBoxChange = () => { + this.navigate(0, Number(this.mPageTextBox.value), 0); + }; + this.mPageTextBox.addEventListener("change", this.mOnPageTextBoxChange); + + let dropdown = document.getElementById("print-preview-scale").menupopup; + for (let menuitem of dropdown.children) { + let value = menuitem.getAttribute("value"); + if (!isNaN(parseFloat(value))) { + document.l10n.setAttributes( + menuitem, + "printpreview-percentage-value", + { percent: Math.round(parseFloat(value) * 100) } + ); + } + } + } + + initialize(aPPBrowser) { + let { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + if (!Services.prefs.getBoolPref("print.use_simplify_page")) { + this.mSimplifyPageCheckbox.hidden = true; + this.mSimplifyPageToolbarSeparator.hidden = true; + } + this.mPPBrowser = aPPBrowser; + this.updateToolbar(); + + let ltr = document.documentElement.matches(":root:-moz-locale-dir(ltr)"); + // Windows 7 doesn't support ⏮ and ⏭ by default, and fallback doesn't + // always work (bug 1343330). + let { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + let useCompatCharacters = AppConstants.isPlatformAndVersionAtMost( + "win", + "6.1" + ); + let leftEnd = useCompatCharacters ? "\u23EA" : "\u23EE"; + let rightEnd = useCompatCharacters ? "\u23E9" : "\u23ED"; + document.l10n.setAttributes( + this.mNavigateHomeButton, + "printpreview-homearrow", + { arrow: ltr ? leftEnd : rightEnd } + ); + document.l10n.setAttributes( + this.mNavigatePreviousButton, + "printpreview-previousarrow", + { arrow: ltr ? "\u25C2" : "\u25B8" } + ); + document.l10n.setAttributes( + this.mNavigateNextButton, + "printpreview-nextarrow", + { arrow: ltr ? "\u25B8" : "\u25C2" } + ); + document.l10n.setAttributes( + this.mNavigateEndButton, + "printpreview-endarrow", + { arrow: ltr ? rightEnd : leftEnd } + ); + } + + destroy() { + delete this.mPPBrowser; + } + + disconnectedCallback() { + window.removeEventListener("unload", this.disconnectedCallback); + this.mPageTextBox.removeEventListener( + "change", + this.mOnPageTextBoxChange + ); + this.destroy(); + } + + disableUpdateTriggers(aDisabled) { + this.mPrintButton.disabled = aDisabled; + this.mPageSetupButton.disabled = aDisabled; + this.mNavigateHomeButton.disabled = aDisabled; + this.mNavigatePreviousButton.disabled = aDisabled; + this.mPageTextBox.disabled = aDisabled; + this.mNavigateNextButton.disabled = aDisabled; + this.mNavigateEndButton.disabled = aDisabled; + this.mScaleCombobox.disabled = aDisabled; + this.mPortaitButton.disabled = aDisabled; + this.mLandscapeButton.disabled = aDisabled; + this.mSimplifyPageCheckbox.disabled = + this.mSimplifyPageNotAllowed || aDisabled; + } + + doPageSetup() { + /* import-globals-from printUtils.js */ + var didOK = PrintUtils.showPageSetup(); + if (didOK) { + // the changes that effect the UI + this.updateToolbar(); + + // Now do PrintPreview + PrintUtils.printPreview(); + } + } + + navigate(aDirection, aPageNum, aHomeOrEnd) { + const nsIWebBrowserPrint = Ci.nsIWebBrowserPrint; + let navType, pageNum; + + let { min: lowerLimit, max: upperLimit } = this.mPageTextBox; + + // we use only one of aHomeOrEnd, aDirection, or aPageNum + if (aHomeOrEnd) { + // We're going to either the very first page ("home"), or the + // very last page ("end"). + if (aHomeOrEnd == "home") { + navType = nsIWebBrowserPrint.PRINTPREVIEW_HOME; + this.mPageTextBox.value = 1; + } else { + navType = nsIWebBrowserPrint.PRINTPREVIEW_END; + this.mPageTextBox.value = upperLimit; + } + pageNum = 0; + } else if (aDirection) { + // aDirection is either +1 or -1, and allows us to increment + // or decrement our currently viewed page. + pageNum = Number(this.mPageTextBox.value) + aDirection; + pageNum = Math.min(upperLimit, Math.max(lowerLimit, pageNum)); + this.mPageTextBox.value = pageNum; + navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM; + } else { + // We're going to a specific page (aPageNum) + navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM; + pageNum = Math.min(upperLimit, Math.max(lowerLimit, aPageNum)); + if (pageNum != aPageNum) { + this.mPageTextBox.value = pageNum; + } + } + + this.mPPBrowser.sendMessageToActor( + "Printing:Preview:Navigate", + { + navType, + pageNum, + }, + "Printing" + ); + } + + print() { + PrintUtils.printWindow(this.mPPBrowser.browsingContext); + } + + promptForScaleValue(aValue) { + var value = Math.round(aValue); + var promptStr = document.getElementById("print-preview-scale-label") + .value; + var renameTitle = document.getElementById( + "print-preview-custom-scale-prompt-title" + ).textContent; + var result = { value }; + let { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + var confirmed = Services.prompt.prompt( + window, + renameTitle, + promptStr, + result, + null, + { value } + ); + if (!confirmed || !result.value || result.value == "") { + return -1; + } + return result.value; + } + + setScaleCombobox(aValue) { + var scaleValues = [ + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1, + 1.25, + 1.5, + 1.75, + 2, + ]; + + aValue = Number(aValue); + + for (var i = 0; i < scaleValues.length; i++) { + if (aValue == scaleValues[i]) { + this.mScaleCombobox.selectedIndex = i; + return; + } + } + this.mScaleCombobox.value = "Custom"; + } + + scale(aValue) { + var settings = PrintUtils.getPrintSettings(); + if (aValue == "ShrinkToFit") { + if (!settings.shrinkToFit) { + settings.shrinkToFit = true; + this.savePrintSettings( + settings, + settings.kInitSaveShrinkToFit | settings.kInitSaveScaling + ); + PrintUtils.printPreview(); + } + return; + } + + if (aValue == "Custom") { + aValue = this.promptForScaleValue(settings.scaling * 100.0); + if (aValue >= 10) { + aValue /= 100.0; + } else { + if (this.mScaleCombobox.hasAttribute("lastValidInx")) { + this.mScaleCombobox.selectedIndex = this.mScaleCombobox.getAttribute( + "lastValidInx" + ); + } + return; + } + } + + this.setScaleCombobox(aValue); + this.mScaleCombobox.setAttribute( + "lastValidInx", + this.mScaleCombobox.selectedIndex + ); + + if (settings.scaling != aValue || settings.shrinkToFit) { + settings.shrinkToFit = false; + settings.scaling = aValue; + this.savePrintSettings( + settings, + settings.kInitSaveShrinkToFit | settings.kInitSaveScaling + ); + PrintUtils.printPreview(); + } + } + + orient(aOrientation) { + const kIPrintSettings = Ci.nsIPrintSettings; + var orientValue = + aOrientation == "portrait" + ? kIPrintSettings.kPortraitOrientation + : kIPrintSettings.kLandscapeOrientation; + var settings = PrintUtils.getPrintSettings(); + if (settings.orientation != orientValue) { + settings.orientation = orientValue; + this.savePrintSettings(settings, settings.kInitSaveOrientation); + PrintUtils.printPreview(); + } + } + + simplify() { + PrintUtils.setSimplifiedMode(this.mSimplifyPageCheckbox.checked); + PrintUtils.printPreview(); + } + + enableSimplifyPage() { + this.mSimplifyPageNotAllowed = false; + this.mSimplifyPageCheckbox.disabled = false; + document.l10n.setAttributes( + this.mSimplifyPageCheckbox, + "printpreview-simplify-page-checkbox-enabled" + ); + } + + disableSimplifyPage() { + this.mSimplifyPageNotAllowed = true; + this.mSimplifyPageCheckbox.disabled = true; + document.l10n.setAttributes( + this.mSimplifyPageCheckbox, + "printpreview-simplify-page-checkbox" + ); + } + + updateToolbar() { + var settings = PrintUtils.getPrintSettings(); + + var isPortrait = + settings.orientation == Ci.nsIPrintSettings.kPortraitOrientation; + + this.mPortaitButton.checked = isPortrait; + this.mLandscapeButton.checked = !isPortrait; + + if (settings.shrinkToFit) { + this.mScaleCombobox.value = "ShrinkToFit"; + } else { + this.setScaleCombobox(settings.scaling); + } + + this.mPageTextBox.value = 1; + } + + savePrintSettings(settings, flags) { + var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + PSSVC.savePrintSettingsToPrefs(settings, true, flags); + } + + updatePageCount(totalPages) { + this.mTotalPages.value = totalPages; + this.mPageTextBox.max = totalPages; + } + }, + { extends: "toolbar" } +); diff --git a/toolkit/components/printing/content/printProgress.js b/toolkit/components/printing/content/printProgress.js new file mode 100644 index 0000000000..0b91f9c3c4 --- /dev/null +++ b/toolkit/components/printing/content/printProgress.js @@ -0,0 +1,230 @@ +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// dialog is just an array we'll use to store various properties from the dialog document... +var dialog; + +// the printProgress is a nsIPrintProgress object +var printProgress = null; + +// random global variables... +var targetFile; + +var docTitle = ""; +var docURL = ""; +var progressParams = null; +var switchUI = true; + +function ellipseString(aStr, doFront) { + if (!aStr) { + return ""; + } + + if ( + aStr.length > 3 && + (aStr.substr(0, 3) == "..." || aStr.substr(aStr.length - 4, 3) == "...") + ) { + return aStr; + } + + var fixedLen = 64; + if (aStr.length > fixedLen) { + if (doFront) { + var endStr = aStr.substr(aStr.length - fixedLen, fixedLen); + return "..." + endStr; + } + var frontStr = aStr.substr(0, fixedLen); + return frontStr + "..."; + } + return aStr; +} + +// all progress notifications are done through the nsIWebProgressListener implementation... +var progressListener = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + // Put progress meter in undetermined mode. + dialog.progress.removeAttribute("value"); + } + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + // we are done printing + // Indicate completion in title area. + document.l10n.setAttributes(dialog.title, "print-complete"); + + // Put progress meter at 100%. + dialog.progress.setAttribute("value", 100); + document.l10n.setAttributes(dialog.progressText, "print-percent", { + percent: 100, + }); + + if (Services.focus.activeWindow == window) { + // This progress dialog is the currently active window. In + // this case we need to make sure that some other window + // gets focus before we close this dialog to work around the + // buggy Windows XP Fax dialog, which ends up parenting + // itself to the currently focused window and is unable to + // survive that window going away. What happens without this + // opener.focus() call on Windows XP is that the fax dialog + // is opened only to go away when this dialog actually + // closes (which can happen asynchronously, so the fax + // dialog just flashes once and then goes away), so w/o this + // fix, it's impossible to fax on Windows XP w/o manually + // switching focus to another window (or holding on to the + // progress dialog with the mouse long enough). + opener.focus(); + } + + window.close(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + if (switchUI) { + dialog.tempLabel.setAttribute("hidden", "true"); + dialog.progressBox.removeAttribute("hidden"); + + switchUI = false; + } + + if (progressParams) { + var docTitleStr = ellipseString(progressParams.docTitle, false); + if (docTitleStr != docTitle) { + docTitle = docTitleStr; + dialog.title.value = docTitle; + } + var docURLStr = progressParams.docURL; + if (docURLStr != docURL && dialog.title != null) { + docURL = docURLStr; + if (docTitle == "") { + dialog.title.value = ellipseString(docURLStr, true); + } + } + } + + // Calculate percentage. + var percent; + if (aMaxTotalProgress > 0) { + percent = Math.round((aCurTotalProgress * 100) / aMaxTotalProgress); + if (percent > 100) { + percent = 100; + } + + // Advance progress meter. + dialog.progress.setAttribute("value", percent); + + // Update percentage label on progress meter. + document.l10n.setAttributes(dialog.progressText, "print-percent", { + percent, + }); + } else { + // Progress meter should be barber-pole in this case. + dialog.progress.removeAttribute("value"); + // Update percentage label on progress meter. + dialog.progressText.setAttribute("value", ""); + } + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + // we can ignore this notification + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + if (aMessage != "") { + dialog.title.setAttribute("value", aMessage); + } + }, + + onSecurityChange(aWebProgress, aRequest, state) { + // we can ignore this notification + }, + + onContentBlockingEvent(aWebProgress, aRequest, event) { + // we can ignore this notification + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +function loadDialog() {} + +function onLoad() { + // Set global variables. + printProgress = window.arguments[0]; + if (window.arguments[1]) { + progressParams = window.arguments[1].QueryInterface( + Ci.nsIPrintProgressParams + ); + if (progressParams) { + docTitle = ellipseString(progressParams.docTitle, false); + docURL = ellipseString(progressParams.docURL, true); + } + } + + if (!printProgress) { + dump("Invalid argument to printProgress.xhtml\n"); + window.close(); + return; + } + + dialog = {}; + dialog.strings = []; + dialog.title = document.getElementById("dialog.title"); + dialog.titleLabel = document.getElementById("dialog.titleLabel"); + dialog.progress = document.getElementById("dialog.progress"); + dialog.progressBox = document.getElementById("dialog.progressBox"); + dialog.progressText = document.getElementById("dialog.progressText"); + dialog.progressLabel = document.getElementById("dialog.progressLabel"); + dialog.tempLabel = document.getElementById("dialog.tempLabel"); + + dialog.progressBox.setAttribute("hidden", "true"); + + document.l10n.setAttributes(dialog.tempLabel, "print-preparing"); + + dialog.title.value = docTitle; + + // Fill dialog. + loadDialog(); + + document.addEventListener("dialogcancel", onCancel); + // set our web progress listener on the helper app launcher + printProgress.registerListener(progressListener); + // We need to delay the set title else dom will overwrite it + window.setTimeout(doneIniting, 500); +} + +function onUnload() { + if (printProgress) { + try { + printProgress.unregisterListener(progressListener); + printProgress = null; + } catch (exception) {} + } +} + +// If the user presses cancel, tell the app launcher and close the dialog. +function onCancel() { + // Cancel app launcher. + try { + printProgress.processCanceledByUser = true; + } catch (exception) {} +} + +function doneIniting() { + printProgress.doneIniting(); +} diff --git a/toolkit/components/printing/content/printProgress.xhtml b/toolkit/components/printing/content/printProgress.xhtml new file mode 100644 index 0000000000..9313fda7ab --- /dev/null +++ b/toolkit/components/printing/content/printProgress.xhtml @@ -0,0 +1,47 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="print-window" + style="width: 36em;" + onload="onLoad()" + onunload="onUnload()"> +<dialog buttons="cancel"> + + <linkset> + <html:link rel="localization" href="toolkit/printing/printDialogs.ftl"/> + </linkset> + + <script src="chrome://global/content/printProgress.js"/> + + <box style="display: grid; grid-template-columns: auto 1fr auto;" flex="1"> + <!-- First row --> + <hbox pack="end"> + <label id="dialog.titleLabel" data-l10n-id="print-title"/> + </hbox> + <label id="dialog.title"/> + <spacer/> + + <!-- Second row --> + <hbox pack="end"> + <html:label id="dialog.progressLabel" for="dialog.progress" + style="margin-right: 1em;" data-l10n-id="progress"/> + </hbox> + <box> + <label id="dialog.tempLabel" data-l10n-id="print-preparing"/> + <vbox pack="center" id="dialog.progressBox" flex="1"> + <html:progress id="dialog.progress" value="0" max="100" style="width: 100%;"/> + </vbox> + </box> + <hbox pack="end" style="min-width: 2.5em;"> + <label id="dialog.progressText"/> + </hbox> + </box> +</dialog> +</window> diff --git a/toolkit/components/printing/content/printUtils.js b/toolkit/components/printing/content/printUtils.js new file mode 100644 index 0000000000..2ecea1c8e0 --- /dev/null +++ b/toolkit/components/printing/content/printUtils.js @@ -0,0 +1,1073 @@ +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ + +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * PrintUtils is a utility for front-end code to trigger common print + * operations (printing, show print preview, show page settings). + * + * Unfortunately, likely due to inconsistencies in how different operating + * systems do printing natively, our XPCOM-level printing interfaces + * are a bit confusing and the method by which we do something basic + * like printing a page is quite circuitous. + * + * To compound that, we need to support remote browsers, and that means + * kicking off the print jobs in the content process. This means we send + * messages back and forth to that process via the Printing actor. + * + * This also means that <xul:browser>'s that hope to use PrintUtils must have + * their type attribute set to "content". + * + * Messages sent: + * + * Printing:Preview:Enter + * This message is sent to put content into print preview mode. We pass + * the content window of the browser we're showing the preview of, and + * the target of the message is the browser that we'll be showing the + * preview in. + * + * Printing:Preview:Exit + * This message is sent to take content out of print preview mode. + */ + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "PRINT_TAB_MODAL", + "print.tab_modal.enabled", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "PRINT_ALWAYS_SILENT", + "print.always_print_silent", + false +); + +ChromeUtils.defineModuleGetter( + this, + "PromptUtils", + "resource://gre/modules/SharedPromptUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "PrintingParent", + "resource://gre/actors/PrintingParent.jsm" +); + +var gFocusedElement = null; + +var gPendingPrintPreviews = new Map(); + +var PrintUtils = { + SAVE_TO_PDF_PRINTER: "Mozilla Save to PDF", + + get _bundle() { + delete this._bundle; + return (this._bundle = Services.strings.createBundle( + "chrome://global/locale/printing.properties" + )); + }, + + /** + * Shows the page setup dialog, and saves any settings changed in + * that dialog if print.save_print_settings is set to true. + * + * @return true on success, false on failure + */ + showPageSetup() { + let printSettings = this.getPrintSettings(); + // If we come directly from the Page Setup menu, the hack in + // _enterPrintPreview will not have been invoked to set the last used + // printer name. For the reasons outlined at that hack, we want that set + // here too. + let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + if (!PSSVC.lastUsedPrinterName) { + if (printSettings.printerName) { + PSSVC.savePrintSettingsToPrefs( + printSettings, + false, + Ci.nsIPrintSettings.kInitSavePrinterName + ); + PSSVC.savePrintSettingsToPrefs( + printSettings, + true, + Ci.nsIPrintSettings.kInitSaveAll + ); + } + } + try { + var PRINTPROMPTSVC = Cc[ + "@mozilla.org/embedcomp/printingprompt-service;1" + ].getService(Ci.nsIPrintingPromptService); + PRINTPROMPTSVC.showPageSetupDialog(window, printSettings, null); + } catch (e) { + dump("showPageSetup " + e + "\n"); + return false; + } + return true; + }, + + getPreviewBrowser(sourceBrowser) { + let dialogBox = gBrowser.getTabDialogBox(sourceBrowser); + for (let dialog of dialogBox.getTabDialogManager()._dialogs) { + let browser = dialog._box.querySelector(".printPreviewBrowser"); + if (browser) { + return browser; + } + } + return null; + }, + + /** + * Updates the hidden state of the "Print preview" and "Page Setup" + * menu items in the file menu depending on the print tab modal pref. + * The print preview menu item is not available on mac. + */ + updatePrintPreviewMenuHiddenState() { + let printPreviewMenuItem = document.getElementById("menu_printPreview"); + if (printPreviewMenuItem) { + printPreviewMenuItem.hidden = PRINT_TAB_MODAL; + } + let pageSetupMenuItem = document.getElementById("menu_printSetup"); + if (pageSetupMenuItem) { + pageSetupMenuItem.hidden = PRINT_TAB_MODAL; + } + }, + + createPreviewBrowsers(aBrowsingContext, aDialogBrowser) { + let _createPreviewBrowser = previewType => { + // When we're not previewing the selection we want to make + // sure that the top-level browser is being printed. + let browsingContext = + previewType == "selection" + ? aBrowsingContext + : aBrowsingContext.top.embedderElement.browsingContext; + let browser = gBrowser.createBrowser({ + remoteType: browsingContext.currentRemoteType, + userContextId: browsingContext.originAttributes.userContextId, + initialBrowsingContextGroupId: browsingContext.group.id, + skipLoad: true, + initiallyActive: true, + }); + browser.addEventListener("DOMWindowClose", function(e) { + // Ignore close events from printing, see the code creating browsers in + // printUtils.js and nsDocumentViewer::OnDonePrinting. + // + // When we print with the new print UI we don't bother creating a new + // <browser> element, so the close event gets dispatched to us. + // + // Ignoring it is harmless (and doesn't cause correctness issues, because + // the preview document can't run script anyways). + e.preventDefault(); + e.stopPropagation(); + }); + browser.addEventListener("contextmenu", function(e) { + e.preventDefault(); + }); + browser.classList.add("printPreviewBrowser"); + browser.setAttribute("flex", "1"); + browser.setAttribute("printpreview", "true"); + browser.setAttribute("previewtype", previewType); + document.l10n.setAttributes(browser, "printui-preview-label"); + return browser; + }; + + let previewStack = document.importNode( + document.getElementById("printPreviewStackTemplate").content, + true + ).firstElementChild; + + let previewBrowser = _createPreviewBrowser("primary"); + previewStack.append(previewBrowser); + let selectionPreviewBrowser; + if (aBrowsingContext.currentRemoteType) { + selectionPreviewBrowser = _createPreviewBrowser("selection"); + previewStack.append(selectionPreviewBrowser); + } + + // show the toolbar after we go into print preview mode so + // that we can initialize the toolbar with total num pages + let previewPagination = document.createElement("printpreview-pagination"); + previewPagination.classList.add("printPreviewNavigation"); + previewStack.append(previewPagination); + + aDialogBrowser.parentElement.prepend(previewStack); + return { previewBrowser, selectionPreviewBrowser }; + }, + + /** + * Opens the tab modal version of the print UI for the current tab. + * + * @param aBrowsingContext + * The BrowsingContext of the window to print. + * @param aExistingPreviewBrowser + * An existing browser created for printing from window.print(). + * @param aPrintInitiationTime + * The time the print was initiated (typically by the user) as obtained + * from `Date.now()`. That is, the initiation time as the number of + * milliseconds since January 1, 1970. + * @param aPrintSelectionOnly + * Whether to print only the active selection of the given browsing + * context. + * @param aPrintFrameOnly + * Whether to print the selected frame only + * @return promise resolving when the dialog is open, rejected if the preview + * fails. + */ + async _openTabModalPrint( + aBrowsingContext, + aExistingPreviewBrowser, + aPrintInitiationTime, + aPrintSelectionOnly, + aPrintFrameOnly + ) { + let hasSelection = aPrintSelectionOnly; + if (!aPrintSelectionOnly) { + let sourceActor = aBrowsingContext.currentWindowGlobal.getActor( + "PrintingSelection" + ); + hasSelection = await sourceActor.sendQuery( + "PrintingSelection:HasSelection" + ); + } + + let sourceBrowser = aBrowsingContext.top.embedderElement; + let previewBrowser = this.getPreviewBrowser(sourceBrowser); + if (previewBrowser) { + // Don't open another dialog if we're already printing. + // + // XXX This can be racy can't it? getPreviewBrowser looks at browser that + // we set up after opening the dialog. But I guess worst case we just + // open two dialogs so... + if (aExistingPreviewBrowser) { + aExistingPreviewBrowser.remove(); + } + return Promise.reject(); + } + + // Create a preview browser. + let args = PromptUtils.objectToPropBag({ + previewBrowser: aExistingPreviewBrowser, + printSelectionOnly: !!aPrintSelectionOnly, + hasSelection, + printFrameOnly: !!aPrintFrameOnly, + }); + let dialogBox = gBrowser.getTabDialogBox(sourceBrowser); + return dialogBox.open( + `chrome://global/content/print.html?browsingContextId=${aBrowsingContext.id}&printInitiationTime=${aPrintInitiationTime}`, + { features: "resizable=no", sizeTo: "available" }, + args + ); + }, + + /** + * Initialize a print, this will open the tab modal UI if it is enabled or + * defer to the native dialog/silent print. + * + * @param aTrigger What triggered the print, in string format, for telemetry + * purposes. + * @param aBrowsingContext + * The BrowsingContext of the window to print. + * Note that the browsing context could belong to a subframe of the + * tab that called window.print, or similar shenanigans. + * @param aOptions + * {openWindowInfo} Non-null if this call comes from window.print(). + * This is the nsIOpenWindowInfo object that has to + * be passed down to createBrowser in order for the + * child process to clone into it. + * {printSelectionOnly} Whether to print only the active selection of + * the given browsing context. + * {printFrameOnly} Whether to print the selected frame. + */ + startPrintWindow(aTrigger, aBrowsingContext, aOptions) { + const printInitiationTime = Date.now(); + let openWindowInfo, printSelectionOnly, printFrameOnly; + if (aOptions) { + ({ openWindowInfo, printSelectionOnly, printFrameOnly } = aOptions); + } + + // When we have a non-null openWindowInfo, we only want to record + // telemetry if we're triggered by window.print() itself, otherwise it's an + // internal print (like the one we do when we actually print from the + // preview dialog, etc.), and that'd cause us to overcount. + if (!openWindowInfo || openWindowInfo.isForWindowDotPrint) { + Services.telemetry.keyedScalarAdd("printing.trigger", aTrigger, 1); + } + + let browser = null; + if (openWindowInfo) { + browser = document.createXULElement("browser"); + browser.openWindowInfo = openWindowInfo; + browser.setAttribute("type", "content"); + let remoteType = aBrowsingContext.currentRemoteType; + if (remoteType) { + browser.setAttribute("remoteType", remoteType); + browser.setAttribute("remote", "true"); + } + // When the print process finishes, we get closed by + // nsDocumentViewer::OnDonePrinting, or by the print preview code. + // + // When that happens, we should remove us from the DOM if connected. + browser.addEventListener("DOMWindowClose", function(e) { + if (browser.isConnected) { + browser.remove(); + } + e.stopPropagation(); + e.preventDefault(); + }); + browser.style.visibility = "collapse"; + document.documentElement.appendChild(browser); + } + + if ( + PRINT_TAB_MODAL && + !PRINT_ALWAYS_SILENT && + (!openWindowInfo || openWindowInfo.isForWindowDotPrint) + ) { + let browsingContext = aBrowsingContext; + let focusedBc = Services.focus.focusedContentBrowsingContext; + if ( + focusedBc && + focusedBc.top.embedderElement == browsingContext.top.embedderElement && + (!openWindowInfo || !openWindowInfo.isForWindowDotPrint) && + !printFrameOnly + ) { + browsingContext = focusedBc; + } + this._openTabModalPrint( + browsingContext, + browser, + printInitiationTime, + printSelectionOnly, + printFrameOnly + ).catch(() => {}); + return browser; + } + + if (browser) { + // Legacy print dialog or silent printing, the content process will print + // in this <browser>. + return browser; + } + + let settings = this.getPrintSettings(); + settings.printSelectionOnly = printSelectionOnly; + this.printWindow(aBrowsingContext, settings); + return null; + }, + + /** + * Starts the process of printing the contents of a window. + * + * @param aBrowsingContext + * The BrowsingContext of the window to print. + * @param {Object?} aPrintSettings + * Optional print settings for the print operation + */ + printWindow(aBrowsingContext, aPrintSettings) { + let windowID = aBrowsingContext.currentWindowGlobal.outerWindowId; + let topBrowser = aBrowsingContext.top.embedderElement; + + const printPreviewIsOpen = !!document.getElementById( + "print-preview-toolbar" + ); + + if (printPreviewIsOpen) { + this._logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PREVIEW"); + } else { + this._logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PAGE"); + } + + // Use the passed in settings if provided, otherwise pull the saved ones. + let printSettings = aPrintSettings || this.getPrintSettings(); + + // Set the title so that the print dialog can pick it up and + // use it to generate the filename for save-to-PDF. + printSettings.title = this._originalTitle || topBrowser.contentTitle; + + if (this._shouldSimplify) { + // The generated document for simplified print preview has "about:blank" + // as its URL. We need to set docURL here so that the print header/footer + // can be given the original document's URL. + printSettings.docURL = this._originalURL || topBrowser.currentURI.spec; + } + + // At some point we should handle the Promise that this returns (report + // rejection to telemetry?) + let promise = topBrowser.print(windowID, printSettings); + + if (printPreviewIsOpen) { + if (this._shouldSimplify) { + this._logKeyedTelemetry("PRINT_COUNT", "SIMPLIFIED"); + } else { + this._logKeyedTelemetry("PRINT_COUNT", "WITH_PREVIEW"); + } + } else { + this._logKeyedTelemetry("PRINT_COUNT", "WITHOUT_PREVIEW"); + } + + return promise; + }, + + /** + * Initializes print preview. + * + * @param aTrigger Optionaly, if it's an external call, what triggered the + * print, in string format, for telemetry purposes. + * @param aListenerObj + * An object that defines the following functions: + * + * getPrintPreviewBrowser: + * Returns the <xul:browser> to display the print preview in. This + * <xul:browser> must have its type attribute set to "content". + * + * getSimplifiedPrintPreviewBrowser: + * Returns the <xul:browser> to display the simplified print preview + * in. This <xul:browser> must have its type attribute set to + * "content". + * + * getSourceBrowser: + * Returns the <xul:browser> that contains the document being + * printed. This <xul:browser> must have its type attribute set to + * "content". + * + * getSimplifiedSourceBrowser: + * Returns the <xul:browser> that contains the simplified version + * of the document being printed. This <xul:browser> must have its + * type attribute set to "content". + * + * getNavToolbox: + * Returns the primary toolbox for this window. + * + * onEnter: + * Called upon entering print preview. + * + * onExit: + * Called upon exiting print preview. + * + * These methods must be defined. printPreview can be called + * with aListenerObj as null iff this window is already displaying + * print preview (in which case, the previous aListenerObj passed + * to it will be used). + * + * Due to a timing issue resulting in a main-process crash, we have to + * manually open the progress dialog for print preview. The progress + * dialog is opened here in PrintUtils, and then we listen for update + * messages from the child. Bug 1558588 is about removing this. + */ + printPreview(aTrigger, aListenerObj) { + if (aTrigger) { + Services.telemetry.keyedScalarAdd("printing.trigger", aTrigger, 1); + } + + if (PRINT_TAB_MODAL) { + let browsingContext = gBrowser.selectedBrowser.browsingContext; + let focusedBc = Services.focus.focusedContentBrowsingContext; + if ( + focusedBc && + focusedBc.top.embedderElement == browsingContext.top.embedderElement + ) { + browsingContext = focusedBc; + } + return this._openTabModalPrint( + browsingContext, + /* aExistingPreviewBrowser = */ undefined, + Date.now() + ); + } + + // If we already have a toolbar someone is calling printPreview() to get us + // to refresh the display and aListenerObj won't be passed. + let printPreviewTB = document.getElementById("print-preview-toolbar"); + if (!printPreviewTB) { + this._listener = aListenerObj; + this._sourceBrowser = aListenerObj.getSourceBrowser(); + this._originalTitle = this._sourceBrowser.contentTitle; + this._originalURL = this._sourceBrowser.currentURI.spec; + + // Here we log telemetry data for when the user enters print preview. + this.logTelemetry("PRINT_PREVIEW_OPENED_COUNT"); + } else { + // Disable toolbar elements that can cause another update to be triggered + // during this update. + printPreviewTB.disableUpdateTriggers(true); + + // collapse the browser here -- it will be shown in + // _enterPrintPreview; this forces a reflow which fixes display + // issues in bug 267422. + // We use the print preview browser as the source browser to avoid + // re-initializing print preview with a document that might now have changed. + this._sourceBrowser = this._shouldSimplify + ? this._listener.getSimplifiedPrintPreviewBrowser() + : this._listener.getPrintPreviewBrowser(); + this._sourceBrowser.collapsed = true; + + // If the user transits too quickly within preview and we have a pending + // progress dialog, we will close it before opening a new one. + this.ensureProgressDialogClosed(); + } + + this._webProgressPP = {}; + let ppParams = {}; + let notifyOnOpen = {}; + let printSettings = this.getPrintSettings(); + // Here we get the PrintingPromptService so we can display the PP Progress from script + // For the browser implemented via XUL with the PP toolbar we cannot let it be + // automatically opened from the print engine because the XUL scrollbars in the PP window + // will layout before the content window and a crash will occur. + // Doing it all from script, means it lays out before hand and we can let printing do its own thing + let PPROMPTSVC = Cc[ + "@mozilla.org/embedcomp/printingprompt-service;1" + ].getService(Ci.nsIPrintingPromptService); + + let promise = new Promise((resolve, reject) => { + this._onEntered.push({ resolve, reject }); + }); + + // just in case we are already printing, + // an error code could be returned if the Progress Dialog is already displayed + try { + PPROMPTSVC.showPrintProgressDialog( + window, + printSettings, + this._obsPP, + false, + this._webProgressPP, + ppParams, + notifyOnOpen + ); + if (ppParams.value) { + ppParams.value.docTitle = this._originalTitle; + ppParams.value.docURL = this._originalURL; + } + + // this tells us whether we should continue on with PP or + // wait for the callback via the observer + if (!notifyOnOpen.value.valueOf() || this._webProgressPP.value == null) { + this._enterPrintPreview(); + } + } catch (e) { + this._enterPrintPreview(); + } + return promise; + }, + + // "private" methods and members. Don't use them. + + _listener: null, + _closeHandlerPP: null, + _webProgressPP: null, + _sourceBrowser: null, + _originalTitle: "", + _originalURL: "", + _shouldSimplify: false, + + _getErrorCodeForNSResult(nsresult) { + const MSG_CODES = [ + "GFX_PRINTER_NO_PRINTER_AVAILABLE", + "GFX_PRINTER_NAME_NOT_FOUND", + "GFX_PRINTER_COULD_NOT_OPEN_FILE", + "GFX_PRINTER_STARTDOC", + "GFX_PRINTER_ENDDOC", + "GFX_PRINTER_STARTPAGE", + "GFX_PRINTER_DOC_IS_BUSY", + "ABORT", + "NOT_AVAILABLE", + "NOT_IMPLEMENTED", + "OUT_OF_MEMORY", + "UNEXPECTED", + ]; + + for (let code of MSG_CODES) { + let nsErrorResult = "NS_ERROR_" + code; + if (Cr[nsErrorResult] == nsresult) { + return code; + } + } + + // PERR_FAILURE is the catch-all error message if we've gotten one that + // we don't recognize. + return "FAILURE"; + }, + + _displayPrintingError(nsresult, isPrinting) { + // The nsresults from a printing error are mapped to strings that have + // similar names to the errors themselves. For example, for error + // NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE, the name of the string + // for the error message is: PERR_GFX_PRINTER_NO_PRINTER_AVAILABLE. What's + // more, if we're in the process of doing a print preview, it's possible + // that there are strings specific for print preview for these errors - + // if so, the names of those strings have _PP as a suffix. It's possible + // that no print preview specific strings exist, in which case it is fine + // to fall back to the original string name. + let msgName = "PERR_" + this._getErrorCodeForNSResult(nsresult); + let msg, title; + if (!isPrinting) { + // Try first with _PP suffix. + let ppMsgName = msgName + "_PP"; + try { + msg = this._bundle.GetStringFromName(ppMsgName); + } catch (e) { + // We allow localizers to not have the print preview error string, + // and just fall back to the printing error string. + } + } + + if (!msg) { + msg = this._bundle.GetStringFromName(msgName); + } + + title = this._bundle.GetStringFromName( + isPrinting + ? "print_error_dialog_title" + : "printpreview_error_dialog_title" + ); + + Services.prompt.alert(window, title, msg); + + Services.telemetry.keyedScalarAdd( + "printing.error", + this._getErrorCodeForNSResult(nsresult), + 1 + ); + }, + + _setPrinterDefaultsForSelectedPrinter( + aPSSVC, + aPrintSettings, + defaultsOnly = false + ) { + if (!aPrintSettings.printerName) { + aPrintSettings.printerName = aPSSVC.lastUsedPrinterName; + if (!aPrintSettings.printerName) { + // It is important to try to avoid passing settings over to the + // content process in the old print UI by saving to unprefixed prefs. + // To avoid that we try to get the name of a printer we can use. + let printerList = Cc["@mozilla.org/gfx/printerlist;1"].getService( + Ci.nsIPrinterList + ); + aPrintSettings.printerName = printerList.systemDefaultPrinterName; + } + } + + // First get any defaults from the printer. We want to skip this for Save to + // PDF since it isn't a real printer and will throw. + if (aPrintSettings.printerName != this.SAVE_TO_PDF_PRINTER) { + aPSSVC.initPrintSettingsFromPrinter( + aPrintSettings.printerName, + aPrintSettings + ); + } + + if (!defaultsOnly) { + // now augment them with any values from last time + aPSSVC.initPrintSettingsFromPrefs( + aPrintSettings, + true, + aPrintSettings.kInitSaveAll + ); + } + }, + + getPrintSettings(aPrinterName, defaultsOnly) { + var printSettings; + try { + var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + printSettings = PSSVC.newPrintSettings; + if (aPrinterName) { + printSettings.printerName = aPrinterName; + } + this._setPrinterDefaultsForSelectedPrinter( + PSSVC, + printSettings, + defaultsOnly + ); + } catch (e) { + dump("getPrintSettings: " + e + "\n"); + } + return printSettings; + }, + + // This observer is called once the progress dialog has been "opened" + _obsPP: { + observe(aSubject, aTopic, aData) { + // Only process a null topic which means the progress dialog is open. + if (aTopic) { + return; + } + + // delay the print preview to show the content of the progress dialog + setTimeout(function() { + PrintUtils._enterPrintPreview(); + }, 0); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + }, + + get shouldSimplify() { + return this._shouldSimplify; + }, + + setSimplifiedMode(shouldSimplify) { + this._shouldSimplify = shouldSimplify; + }, + + _onEntered: [], + + /** + * Currently, we create a new print preview browser to host the simplified + * cloned-document when Simplify Page option is used on preview. To accomplish + * this, we need to keep track of what browser should be presented, based on + * whether the 'Simplify page' checkbox is checked. + * + * _ppBrowsers + * Set of print preview browsers. + * _currentPPBrowser + * References the current print preview browser that is being presented. + */ + _ppBrowsers: new Set(), + _currentPPBrowser: null, + + _enterPrintPreview() { + // Send a message to the print preview browser to initialize + // print preview. If we happen to have gotten a print preview + // progress listener from nsIPrintingPromptService.showPrintProgressDialog + // in printPreview, we add listeners to feed that progress + // listener. + let ppBrowser = this._shouldSimplify + ? this._listener.getSimplifiedPrintPreviewBrowser() + : this._listener.getPrintPreviewBrowser(); + this._ppBrowsers.add(ppBrowser); + + // If we're switching from 'normal' print preview to 'simplified' print + // preview, we will want to run reader mode against the 'normal' print + // preview browser's content: + let oldPPBrowser = null; + let changingPrintPreviewBrowsers = false; + if (this._currentPPBrowser && ppBrowser != this._currentPPBrowser) { + changingPrintPreviewBrowsers = true; + oldPPBrowser = this._currentPPBrowser; + } + this._currentPPBrowser = ppBrowser; + + let waitForPrintProgressToEnableToolbar = false; + if (this._webProgressPP.value) { + waitForPrintProgressToEnableToolbar = true; + } + + gPendingPrintPreviews.set(ppBrowser, waitForPrintProgressToEnableToolbar); + + // If we happen to have gotten simplify page checked, we will lazily + // instantiate a new tab that parses the original page using ReaderMode + // primitives. When it's ready, and in order to enter on preview, we send + // over a message to print preview browser passing up the simplified tab as + // reference. If not, we pass the original tab instead as content source. + if (this._shouldSimplify) { + let simplifiedBrowser = this._listener.getSimplifiedSourceBrowser(); + if (!simplifiedBrowser) { + simplifiedBrowser = this._listener.createSimplifiedBrowser(); + + // Here, we send down a message to simplified browser in order to parse + // the original page. After we have parsed it, content will tell parent + // that the document is ready for print previewing. + simplifiedBrowser.sendMessageToActor( + "Printing:Preview:ParseDocument", + { + URL: this._originalURL, + windowID: oldPPBrowser.outerWindowID, + }, + "Printing" + ); + + // Here we log telemetry data for when the user enters simplify mode. + this.logTelemetry("PRINT_PREVIEW_SIMPLIFY_PAGE_OPENED_COUNT"); + + return; + } + } + + this.sendEnterPrintPreviewToChild( + ppBrowser, + this._sourceBrowser, + this._shouldSimplify, + changingPrintPreviewBrowsers + ); + }, + + sendEnterPrintPreviewToChild( + ppBrowser, + sourceBrowser, + simplifiedMode, + changingBrowsers + ) { + ppBrowser.sendMessageToActor( + "Printing:Preview:Enter", + { + browsingContextId: sourceBrowser.browsingContext.id, + simplifiedMode, + changingBrowsers, + lastUsedPrinterName: this.getLastUsedPrinterName(), + }, + "Printing" + ); + }, + + printPreviewEntered(ppBrowser, previewResult) { + let waitForPrintProgressToEnableToolbar = gPendingPrintPreviews.get( + ppBrowser + ); + gPendingPrintPreviews.delete(ppBrowser); + + for (let { resolve, reject } of this._onEntered) { + if (previewResult.failed) { + reject(); + } else { + resolve(); + } + } + + this._onEntered = []; + if (previewResult.failed) { + // Something went wrong while putting the document into print preview + // mode. Bail out. + this._ppBrowsers.clear(); + this._listener.onEnter(); + this._listener.onExit(); + return; + } + + // Stash the focused element so that we can return to it after exiting + // print preview. + gFocusedElement = document.commandDispatcher.focusedElement; + + let printPreviewTB = document.getElementById("print-preview-toolbar"); + if (printPreviewTB) { + if (previewResult.changingBrowsers) { + printPreviewTB.destroy(); + printPreviewTB.initialize(ppBrowser); + } else { + // printPreviewTB.initialize above already calls updateToolbar. + printPreviewTB.updateToolbar(); + } + + // If we don't have a progress listener to enable the toolbar do it now. + if (!waitForPrintProgressToEnableToolbar) { + printPreviewTB.disableUpdateTriggers(false); + } + + ppBrowser.collapsed = false; + ppBrowser.focus(); + return; + } + + // Set the original window as an active window so any mozPrintCallbacks can + // run without delayed setTimeouts. + if (this._listener.activateBrowser) { + this._listener.activateBrowser(this._sourceBrowser); + } else { + this._sourceBrowser.docShellIsActive = true; + } + + // show the toolbar after we go into print preview mode so + // that we can initialize the toolbar with total num pages + printPreviewTB = document.createXULElement("toolbar", { + is: "printpreview-toolbar", + }); + printPreviewTB.setAttribute("fullscreentoolbar", true); + printPreviewTB.setAttribute("flex", "1"); + printPreviewTB.id = "print-preview-toolbar"; + + let navToolbox = this._listener.getNavToolbox(); + navToolbox.parentNode.insertBefore(printPreviewTB, navToolbox); + printPreviewTB.initialize(ppBrowser); + + // The print preview processing may not have fully completed, so if we + // have a progress listener, disable the toolbar elements that can trigger + // updates and it will enable them when completed. + if (waitForPrintProgressToEnableToolbar) { + printPreviewTB.disableUpdateTriggers(true); + } + + // Enable simplify page checkbox when the page is an article + if (this._sourceBrowser.isArticle) { + printPreviewTB.enableSimplifyPage(); + } else { + this.logTelemetry("PRINT_PREVIEW_SIMPLIFY_PAGE_UNAVAILABLE_COUNT"); + printPreviewTB.disableSimplifyPage(); + } + + // copy the window close handler + if (window.onclose) { + this._closeHandlerPP = window.onclose; + } else { + this._closeHandlerPP = null; + } + window.onclose = function() { + PrintUtils.exitPrintPreview(); + return false; + }; + + // disable chrome shortcuts... + window.addEventListener("keydown", this.onKeyDownPP, true); + window.addEventListener("keypress", this.onKeyPressPP, true); + + ppBrowser.collapsed = false; + ppBrowser.focus(); + // on Enter PP Call back + this._listener.onEnter(); + }, + + readerModeReady(sourceBrowser) { + let ppBrowser = this._listener.getSimplifiedPrintPreviewBrowser(); + this.sendEnterPrintPreviewToChild(ppBrowser, sourceBrowser, true, true); + }, + + getLastUsedPrinterName() { + let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + let lastUsedPrinterName = PSSVC.lastUsedPrinterName; + if (!lastUsedPrinterName) { + // We "pass" print settings over to the content process by saving them to + // prefs (yuck!). It is important to try to avoid saving to prefs without + // prefixing them with a printer name though, so this hack tries to make + // sure that (in the common case) we have set the "last used" printer, + // which makes us save to prefs prefixed with its name, and makes sure + // the content process will pick settings up from those prefixed prefs + // too. + let settings = this.getPrintSettings(); + if (settings.printerName) { + PSSVC.savePrintSettingsToPrefs( + settings, + false, + Ci.nsIPrintSettings.kInitSavePrinterName + ); + PSSVC.savePrintSettingsToPrefs( + settings, + true, + Ci.nsIPrintSettings.kInitSaveAll + ); + lastUsedPrinterName = settings.printerName; + } + } + + return lastUsedPrinterName; + }, + + exitPrintPreview() { + for (let browser of this._ppBrowsers) { + browser.sendMessageToActor("Printing:Preview:Exit", {}, "Printing"); + } + this._ppBrowsers.clear(); + this._currentPPBrowser = null; + window.removeEventListener("keydown", this.onKeyDownPP, true); + window.removeEventListener("keypress", this.onKeyPressPP, true); + + // restore the old close handler + if (this._closeHandlerPP) { + window.onclose = this._closeHandlerPP; + } else { + window.onclose = null; + } + this._closeHandlerPP = null; + + // remove the print preview toolbar + let printPreviewTB = document.getElementById("print-preview-toolbar"); + printPreviewTB.destroy(); + printPreviewTB.remove(); + + if (gFocusedElement) { + Services.focus.setFocus(gFocusedElement, Services.focus.FLAG_NOSCROLL); + } else { + this._sourceBrowser.focus(); + } + gFocusedElement = null; + + this.setSimplifiedMode(false); + + this.ensureProgressDialogClosed(); + + this._listener.onExit(); + + this._originalTitle = ""; + this._originalURL = ""; + }, + + logTelemetry(ID) { + let histogram = Services.telemetry.getHistogramById(ID); + histogram.add(true); + }, + + _logKeyedTelemetry(id, key) { + let histogram = Services.telemetry.getKeyedHistogramById(id); + histogram.add(key); + }, + + onKeyDownPP(aEvent) { + // Esc exits the PP + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + PrintUtils.exitPrintPreview(); + } + }, + + onKeyPressPP(aEvent) { + var closeKey; + try { + closeKey = document.getElementById("key_close").getAttribute("key"); + closeKey = aEvent["DOM_VK_" + closeKey]; + } catch (e) {} + var isModif = aEvent.ctrlKey || aEvent.metaKey; + // Ctrl-W exits the PP + if ( + isModif && + (aEvent.charCode == closeKey || aEvent.charCode == closeKey + 32) + ) { + PrintUtils.exitPrintPreview(); + } else if (isModif) { + var printPreviewTB = document.getElementById("print-preview-toolbar"); + var printKey = document + .getElementById("printKb") + .getAttribute("key") + .toUpperCase(); + var pressedKey = String.fromCharCode(aEvent.charCode).toUpperCase(); + if (printKey == pressedKey) { + printPreviewTB.print(); + } + } + // cancel shortkeys + if (isModif) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + }, + + /** + * If there's a printing or print preview progress dialog displayed, force + * it to close now. + */ + ensureProgressDialogClosed() { + if (this._webProgressPP && this._webProgressPP.value) { + this._webProgressPP.value.onStateChange( + null, + null, + Ci.nsIWebProgressListener.STATE_STOP, + 0 + ); + } + }, +}; diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css new file mode 100644 index 0000000000..257acad91b --- /dev/null +++ b/toolkit/components/printing/content/simplifyMode.css @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This file defines specific rules for print preview when using simplify mode. + * Some of these rules (styling for title and author on the header element) + * already exist in aboutReader.css, however, we decoupled it from the original + * file so we don't need to load a bunch of extra queries that will not take + * effect when using the simplify page checkbox. */ + +body { + padding-top: 0px; + padding-bottom: 0px; +} + +#container { + max-width: 100%; + font-family: Georgia, "Times New Roman", serif; +} + +.header > h1 { + font-size: 1.6em; + line-height: 1.25em; + margin: 30px 0; +} + +.header > .credits { + font-size: 0.9em; + line-height: 1.48em; + margin: 0 0 30px 0; + font-style: italic; +} diff --git a/toolkit/components/printing/content/toggle-group.css b/toolkit/components/printing/content/toggle-group.css new file mode 100644 index 0000000000..fb28981a77 --- /dev/null +++ b/toolkit/components/printing/content/toggle-group.css @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +/* + * A radiogroup styled to hide the radio buttons + * and present tab-like labels as buttons + */ + +.toggle-group { + display: inline-flex; +} + +.toggle-group-label { + display: inline-flex; + align-items: center; + margin: 0; + padding: 6px 10px; + background-color: var(--in-content-button-background); +} + +.toggle-group-input { + position: absolute; + inset-inline-start: -100px; +} + +.toggle-group-label-iconic::before { + width: 16px; + height: 16px; + margin-inline-end: 5px; + -moz-context-properties: fill; + fill: currentColor; +} + +.toggle-group-label:first-of-type { + border-start-start-radius: 2px; + border-end-start-radius: 2px; +} + +.toggle-group-label:last-of-type { + border-start-end-radius: 2px; + border-end-end-radius: 2px; +} + +.toggle-group-input:not(:enabled) + .toggle-group-label { + opacity: 0.7; +} + +.toggle-group-input:enabled + .toggle-group-label:hover { + background-color: var(--in-content-button-background-hover); +} + +.toggle-group-input:enabled + .toggle-group-label:active { + background-color: var(--in-content-button-background-active); +} + +.toggle-group-input:-moz-focusring + .toggle-group-label { + box-shadow: 0 0 0 1px var(--border-color), 0 0 0 4px rgba(10, 132, 255, 0.3); + outline: none; + z-index: 1; +} + +.toggle-group-input:checked:-moz-focusring + .toggle-group-label { + box-shadow: 0 0 0 1px var(--in-content-border-active), 0 0 0 4px rgba(10, 132, 255, 0.3); +} + +.toggle-group-input:enabled:checked + .toggle-group-label{ + background-color: var(--in-content-primary-button-background); + color: var(--in-content-selected-text); +} + +.toggle-group-input:enabled:checked + .toggle-group-label:hover { + background-color: var(--in-content-primary-button-background-hover); +} + +.toggle-group-input:enabled:checked + .toggle-group-label:active { + background-color: var(--in-content-primary-button-background-active); +} |