blob: 91832cc5c40879030f48198e834f42550f4f7cd2 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '/strings.m.js';
import '../data/document_info.js';
import '../data/model.js';
import '../data/state.js';
import './preview_area.js';
import './sidebar.js';
import {WebUiListenerMixinLit} from 'chrome://resources/cr_elements/web_ui_listener_mixin_lit.js';
import {assert} from 'chrome://resources/js/assert.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
import {isMac, isWindows} from 'chrome://resources/js/platform.js';
import {hasKeyModifiers} from 'chrome://resources/js/util.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {Destination} from '../data/destination.js';
import {PrinterType} from '../data/destination.js';
import type {DocumentSettings, PrintPreviewDocumentInfoElement} from '../data/document_info.js';
import {createDocumentSettings} from '../data/document_info.js';
import type {Margins} from '../data/margins.js';
import {MeasurementSystem} from '../data/measurement_system.js';
import type {PrintPreviewModelElement} from '../data/model.js';
import {DuplexMode, whenReady} from '../data/model.js';
import {Size} from '../data/size.js';
import type {PrintPreviewStateElement} from '../data/state.js';
import {Error, State} from '../data/state.js';
import type {NativeInitialSettings, NativeLayer} from '../native_layer.js';
import {NativeLayerImpl} from '../native_layer.js';
import {getCss} from './app.css.js';
import {getHtml} from './app.html.js';
import {DestinationState} from './destination_settings.js';
import type {PrintPreviewPreviewAreaElement} from './preview_area.js';
import {PreviewAreaState} from './preview_area.js';
import {SettingsMixin} from './settings_mixin.js';
import type {PrintPreviewSidebarElement} from './sidebar.js';
export interface PrintPreviewAppElement {
$: {
documentInfo: PrintPreviewDocumentInfoElement,
model: PrintPreviewModelElement,
previewArea: PrintPreviewPreviewAreaElement,
sidebar: PrintPreviewSidebarElement,
state: PrintPreviewStateElement,
};
}
const PrintPreviewAppElementBase =
WebUiListenerMixinLit(SettingsMixin(CrLitElement));
export class PrintPreviewAppElement extends PrintPreviewAppElementBase {
static get is() {
return 'print-preview-app';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
state: {type: Number},
controlsManaged_: {type: Boolean},
destination_: {type: Object},
destinationsManaged_: {type: Boolean},
documentSettings_: {type: Object},
error_: {type: Number},
margins_: {type: Object},
pageSize_: {type: Object},
settingsManaged_: {type: Boolean},
measurementSystem_: {type: Object},
};
}
accessor state: State = State.NOT_READY;
protected accessor controlsManaged_: boolean = false;
protected accessor destination_: Destination|null = null;
private accessor destinationsManaged_: boolean = false;
protected accessor documentSettings_: DocumentSettings =
createDocumentSettings();
protected accessor error_: Error|null = null;
protected accessor margins_: Margins|null = null;
protected accessor pageSize_: Size = new Size(612, 792);
protected accessor settingsManaged_: boolean = false;
protected accessor measurementSystem_: MeasurementSystem|null = null;
private nativeLayer_: NativeLayer|null = null;
private tracker_: EventTracker = new EventTracker();
private cancelled_: boolean = false;
private printRequested_: boolean = false;
private startPreviewWhenReady_: boolean = false;
private showSystemDialogBeforePrint_: boolean = false;
private openPdfInPreview_: boolean = false;
private isInKioskAutoPrintMode_: boolean = false;
private whenReady_: Promise<void>|null = null;
constructor() {
super();
// Regular expression that captures the leading slash, the content and the
// trailing slash in three different groups.
const CANONICAL_PATH_REGEX = /(^\/)([\/-\w]+)(\/$)/;
const path = location.pathname.replace(CANONICAL_PATH_REGEX, '$1$2');
if (path !== '/') { // There are no subpages in Print Preview.
window.history.replaceState(undefined /* stateObject */, '', '/');
}
}
override firstUpdated() {
FocusOutlineManager.forDocument(document);
// <if expr="is_macosx">
this.$.sidebar.addEventListener(
'open-pdf-in-preview', this.onOpenPdfInPreview_.bind(this));
// </if>
}
override connectedCallback() {
super.connectedCallback();
document.documentElement.classList.remove('loading');
this.nativeLayer_ = NativeLayerImpl.getInstance();
this.addWebUiListener(
'print-preset-options', this.onPrintPresetOptions_.bind(this));
this.tracker_.add(window, 'keydown', this.onKeyDown_.bind(this));
this.$.previewArea.setPluginKeyEventCallback(this.onKeyDown_.bind(this));
this.whenReady_ = whenReady();
this.nativeLayer_.getInitialSettings().then(
this.onInitialSettingsSet_.bind(this));
}
override disconnectedCallback() {
super.disconnectedCallback();
this.tracker_.removeAll();
this.whenReady_ = null;
}
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('destinationsManaged_') ||
changedPrivateProperties.has('settingsManaged_')) {
this.controlsManaged_ =
this.destinationsManaged_ || this.settingsManaged_;
}
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedProperties.has('state')) {
this.updateUiForStateChange_();
}
if (changedPrivateProperties.has('error_')) {
if (this.error_ !== null && this.error_ !== Error.NONE) {
this.nativeLayer_!.recordInHistogram(
'PrintPreview.StateError', this.error_, Error.MAX_BUCKET);
}
}
}
protected onSidebarFocus_() {
this.$.previewArea.hideToolbar();
}
/**
* Consume escape and enter key presses and ctrl + shift + p. Delegate
* everything else to the preview area.
*/
private onKeyDown_(e: KeyboardEvent) {
if (e.key === 'Escape' && !hasKeyModifiers(e)) {
// On non-mac with toolkit-views, ESC key is handled by C++-side instead
// of JS-side.
if (isMac) {
this.close_();
e.preventDefault();
}
return;
}
// On Mac, Cmd+Period should close the print dialog.
if (isMac && e.key === '.' && e.metaKey) {
this.close_();
e.preventDefault();
return;
}
// Ctrl + Shift + p / Mac equivalent. Doesn't apply on Chrome OS.
// On Linux/Windows, shift + p means that e.key will be 'P' with caps lock
// off or 'p' with caps lock on.
// On Mac, alt + p means that e.key will be unicode 03c0 (pi).
if (e.key === 'P' || e.key === 'p' || e.key === '\u03c0') {
if ((isMac && e.metaKey && e.altKey && !e.shiftKey && !e.ctrlKey) ||
(!isMac && e.shiftKey && e.ctrlKey && !e.altKey && !e.metaKey)) {
// Don't use system dialog if the link isn't available.
if (!this.$.sidebar.systemDialogLinkAvailable()) {
return;
}
// Don't try to print with system dialog on Windows if the document is
// not ready, because we send the preview document to the printer on
// Windows.
if (!isWindows || this.state === State.READY) {
this.onPrintWithSystemDialog_();
}
e.preventDefault();
return;
}
}
if ((e.key === 'Enter' || e.key === 'NumpadEnter') &&
this.state === State.READY) {
const activeElement = e.composedPath()[0] as HTMLElement | undefined;
// activeElement may be undefined if this is a forwarded key event from
// the plugin. Print Preview conventionally does not trigger a print for
// Enter when the plugin is focused
if (!activeElement) {
return;
}
const activeElementTag = activeElement.tagName;
if (['CR-BUTTON', 'BUTTON', 'SELECT', 'A', 'CR-CHECKBOX'].includes(
activeElementTag)) {
return;
}
this.onPrintRequested_();
e.preventDefault();
return;
}
// Pass certain directional keyboard events to the PDF viewer.
this.$.previewArea.handleDirectionalKeyEvent(e);
}
private onInitialSettingsSet_(settings: NativeInitialSettings) {
if (!this.whenReady_) {
// This element and its corresponding model were detached while waiting
// for the callback. This can happen in tests; return early.
return;
}
this.whenReady_.then(() => {
this.$.documentInfo.init(
settings.previewModifiable, settings.documentTitle,
settings.documentHasSelection);
this.$.model.setStickySettings(settings.serializedAppStateStr);
this.$.model.setPolicySettings(settings.policies);
this.measurementSystem_ = new MeasurementSystem(
settings.thousandsDelimiter, settings.decimalDelimiter,
settings.unitType);
this.setSetting('selectionOnly', settings.shouldPrintSelectionOnly);
this.$.sidebar.init(
settings.isInAppKioskMode, settings.printerName,
settings.serializedDefaultDestinationSelectionRulesStr,
settings.pdfPrinterDisabled);
this.destinationsManaged_ = settings.destinationsManaged;
this.isInKioskAutoPrintMode_ = settings.isInKioskAutoPrintMode;
// This is only visible in the task manager.
let title = document.head.querySelector('title');
if (!title) {
title = document.createElement('title');
document.head.appendChild(title);
}
title.textContent = settings.documentTitle;
});
}
protected onDestinationStateChanged_(
e: CustomEvent<{value: DestinationState}>) {
const destinationState = e.detail.value;
switch (destinationState) {
case DestinationState.SET:
if (this.state !== State.NOT_READY &&
this.state !== State.FATAL_ERROR) {
this.$.state.transitTo(State.NOT_READY);
}
break;
case DestinationState.UPDATED:
if (!this.$.model.initialized()) {
this.$.model.applyStickySettings();
}
this.$.model.applyPoliciesOnDestinationUpdate();
this.startPreviewWhenReady_ = true;
if (this.state === State.NOT_READY &&
this.destination_!.type !== PrinterType.PDF_PRINTER) {
this.nativeLayer_!.recordBooleanHistogram(
'PrintPreview.TransitionedToReadyState', true);
}
this.$.state.transitTo(State.READY);
break;
case DestinationState.ERROR:
if (this.state === State.NOT_READY &&
this.destination_!.type !== PrinterType.PDF_PRINTER) {
this.nativeLayer_!.recordBooleanHistogram(
'PrintPreview.TransitionedToReadyState', false);
}
this.$.state.transitTo(State.ERROR);
break;
default:
break;
}
}
/**
* @param e Event containing the new sticky settings.
*/
protected onStickySettingChanged_(e: CustomEvent<string>) {
this.nativeLayer_!.saveAppState(e.detail);
}
protected async onPreviewSettingChanged_() {
if (this.state === State.READY) {
// Need to wait for rendering to finish, to ensure that the `destination`
// is synced across print-preview-app, print-preview-model and
// print-preview-area.
await this.updateComplete;
assert(this.destination_!.id === this.$.previewArea.destination!.id);
assert(this.destination_!.id === this.$.model.destination!.id);
this.$.previewArea.startPreview(false);
this.startPreviewWhenReady_ = false;
} else {
this.startPreviewWhenReady_ = true;
}
}
private updateUiForStateChange_() {
if (this.state === State.READY) {
if (this.startPreviewWhenReady_) {
this.$.previewArea.startPreview(false);
this.startPreviewWhenReady_ = false;
}
if (this.isInKioskAutoPrintMode_ || this.printRequested_) {
this.onPrintRequested_();
// Reset in case printing fails.
this.printRequested_ = false;
}
} else if (this.state === State.CLOSING) {
this.remove();
this.nativeLayer_!.dialogClose(this.cancelled_);
} else if (this.state === State.PRINT_PENDING) {
assert(this.destination_);
if (this.destination_.type !== PrinterType.PDF_PRINTER) {
// Only hide the preview for local, non PDF destinations.
this.nativeLayer_!.hidePreview();
this.$.state.transitTo(State.HIDDEN);
}
} else if (this.state === State.PRINTING) {
assert(this.destination_);
const whenPrintDone =
this.nativeLayer_!.doPrint(this.$.model.createPrintTicket(
this.destination_, this.openPdfInPreview_,
this.showSystemDialogBeforePrint_));
const onError = this.destination_.type === PrinterType.PDF_PRINTER ?
this.onFileSelectionCancel_.bind(this) :
this.onPrintFailed_.bind(this);
whenPrintDone.then(this.close_.bind(this), onError);
}
}
protected onPrintRequested_() {
if (this.state === State.NOT_READY) {
this.printRequested_ = true;
return;
}
this.$.state.transitTo(
this.$.previewArea.previewLoaded() ? State.PRINTING :
State.PRINT_PENDING);
}
protected onCancelRequested_() {
this.cancelled_ = true;
this.$.state.transitTo(State.CLOSING);
}
/**
* @param e The event containing the new validity.
*/
protected onSettingValidChanged_(e: CustomEvent<boolean>) {
if (e.detail) {
this.$.state.transitTo(State.READY);
} else {
this.error_ = Error.INVALID_TICKET;
this.$.state.transitTo(State.ERROR);
}
}
private onFileSelectionCancel_() {
this.$.state.transitTo(State.READY);
}
protected onPrintWithSystemDialog_() {
// <if expr="is_win">
this.showSystemDialogBeforePrint_ = true;
this.onPrintRequested_();
// </if>
// <if expr="not is_win">
this.nativeLayer_!.showSystemDialog();
this.$.state.transitTo(State.SYSTEM_DIALOG);
// </if>
}
// <if expr="is_macosx">
protected onOpenPdfInPreview_() {
this.openPdfInPreview_ = true;
this.$.previewArea.setOpeningPdfInPreview();
this.onPrintRequested_();
}
// </if>
/**
* Called when printing to an extension printer fails.
* @param httpError The HTTP error code, or -1 or a string describing
* the error, if not an HTTP error.
*/
private onPrintFailed_(httpError: number|string) {
console.warn('Printing failed with error code ' + httpError);
this.error_ = Error.PRINT_FAILED;
this.$.state.transitTo(State.FATAL_ERROR);
}
protected onPreviewStateChanged_(e: CustomEvent<{value: PreviewAreaState}>) {
const previewState = e.detail.value;
switch (previewState) {
case PreviewAreaState.DISPLAY_PREVIEW:
case PreviewAreaState.OPEN_IN_PREVIEW_LOADED:
if (this.state === State.PRINT_PENDING || this.state === State.HIDDEN) {
this.$.state.transitTo(State.PRINTING);
}
break;
case PreviewAreaState.ERROR:
if (this.state !== State.ERROR && this.state !== State.FATAL_ERROR) {
this.$.state.transitTo(
this.error_ === Error.INVALID_PRINTER ? State.ERROR :
State.FATAL_ERROR);
}
break;
default:
break;
}
}
/**
* Updates printing options according to source document presets.
* @param disableScaling Whether the document disables scaling.
* @param copies The default number of copies from the document.
* @param duplex The default duplex setting from the document.
*/
private onPrintPresetOptions_(
disableScaling: boolean, copies: number, duplex: DuplexMode) {
if (disableScaling) {
this.$.documentInfo.updateIsScalingDisabled(true);
}
if (copies > 0 && this.getSetting('copies').available) {
this.setSetting('copies', copies, true);
}
if (duplex === DuplexMode.UNKNOWN_DUPLEX_MODE) {
return;
}
if (this.getSetting('duplex').available) {
this.setSetting(
'duplex',
duplex === DuplexMode.LONG_EDGE || duplex === DuplexMode.SHORT_EDGE,
true);
}
if (duplex !== DuplexMode.SIMPLEX &&
this.getSetting('duplexShortEdge').available) {
this.setSetting(
'duplexShortEdge', duplex === DuplexMode.SHORT_EDGE, true);
}
}
/**
* @param e Contains the new preview request ID.
*/
protected onPreviewStart_(e: CustomEvent<number>) {
this.$.documentInfo.inFlightRequestId = e.detail;
}
private close_() {
this.$.state.transitTo(State.CLOSING);
}
protected onDestinationChanged_(e: CustomEvent<{value: Destination}>) {
this.destination_ = e.detail.value;
}
protected onDestinationCapabilitiesChanged_() {
this.$.model.updateSettingsFromDestination();
}
protected onStateChanged_(e: CustomEvent<{value: State}>) {
this.state = e.detail.value;
}
protected onErrorChanged_(e: CustomEvent<{value: Error}>) {
this.error_ = e.detail.value;
}
protected onSettingsManagedChanged_(e: CustomEvent<{value: boolean}>) {
this.settingsManaged_ = e.detail.value;
}
protected onDocumentSettingsChanged_(
e: CustomEvent<{value: DocumentSettings}>) {
this.documentSettings_ = e.detail.value;
}
protected onMarginsChanged_(e: CustomEvent<{value: Margins}>) {
this.margins_ = e.detail.value;
}
protected onPageSizeChanged_(e: CustomEvent<{value: Size}>) {
this.pageSize_ = e.detail.value;
}
}
export type AppElement = PrintPreviewAppElement;
declare global {
interface HTMLElementTagNameMap {
'print-preview-app': PrintPreviewAppElement;
}
}
customElements.define(PrintPreviewAppElement.is, PrintPreviewAppElement);