blob: 0cac7a338c7cc46799577b0b8557a472bfa45849 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chrome://resources/js/assert_ts.js';
// <if expr="is_chromeos">
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
// </if>
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BackgroundGraphicsModeRestriction, Policies} from '../native_layer.js';
// <if expr="is_chromeos">
import {ColorModeRestriction, DuplexModeRestriction, PinModeRestriction} from '../native_layer.js';
// </if>
import {CapabilityWithReset, Cdd, CddCapabilities, ColorOption, DpiOption, DuplexOption, MediaSizeOption} from './cdd.js';
import {Destination, DestinationOrigin, GooglePromotedDestinationId, PrinterType, RecentDestination} from './destination.js';
import {DocumentSettings} from './document_info.js';
import {CustomMarginsOrientation, Margins, MarginsSetting, MarginsType} from './margins.js';
import {ScalingType} from './scaling.js';
import {Size} from './size.js';
/**
* |key| is the field in the serialized settings state that corresponds to the
* setting, or an empty string if the setting should not be saved in the
* serialized state.
*/
export interface Setting {
value: any;
unavailableValue: any;
valid: boolean;
available: boolean;
setByPolicy: boolean;
setFromUi: boolean;
key: string;
updatesPreview: boolean;
}
export interface Settings {
pages: Setting;
copies: Setting;
collate: Setting;
layout: Setting;
color: Setting;
customMargins: Setting;
mediaSize: Setting;
margins: Setting;
dpi: Setting;
scaling: Setting;
scalingType: Setting;
scalingTypePdf: Setting;
duplex: Setting;
duplexShortEdge: Setting;
cssBackground: Setting;
selectionOnly: Setting;
headerFooter: Setting;
rasterize: Setting;
vendorItems: Setting;
otherOptions: Setting;
ranges: Setting;
pagesPerSheet: Setting;
// <if expr="is_chromeos">
pin: Setting;
pinValue: Setting;
// </if>
}
export interface SerializedSettings {
version: number;
recentDestinations?: RecentDestination[];
dpi?: DpiOption;
mediaSize?: MediaSizeOption;
marginsType?: MarginsType;
customMargins?: MarginsSetting;
isColorEnabled?: boolean;
isDuplexEnabled?: boolean;
isDuplexShortEdge?: boolean;
isHeaderFooterEnabled?: boolean;
isLandscapeEnabled?: boolean;
isCollateEnabled?: boolean;
isCssBackgroundEnabled?: boolean;
scaling?: string;
scalingType?: ScalingType;
scalingTypePdf?: ScalingType;
vendorOptions?: object;
// <if expr="is_chromeos">
isPinEnabled?: boolean;
pinValue?: string;
// </if>
}
export interface PolicyEntry {
value: any;
managed: boolean;
applyOnDestinationUpdate: boolean;
}
export interface PolicyObjectEntry {
defaultMode?: any;
allowedMode?: any;
value?: number;
}
export interface PolicySettings {
headerFooter?: PolicyEntry;
cssBackground?: PolicyEntry;
mediaSize?: PolicyEntry;
sheets?: PolicyEntry;
color?: PolicyEntry;
duplex?: PolicyEntry;
pin?: PolicyEntry;
printPdfAsImageAvailability?: PolicyEntry;
printPdfAsImage?: PolicyEntry;
}
interface CloudJobTicketPrint {
page_orientation?: object;
dpi?: object;
vendor_ticket_item?: object[];
copies?: object;
media_size?: object;
duplex?: object;
color?: {vendor_id?: string, type?: string};
collate?: object;
}
interface CloudJobTicket {
version: string;
print: CloudJobTicketPrint;
}
export interface MediaSizeValue {
width_microns: number;
height_microns: number;
}
export interface Ticket {
collate: boolean;
color: number;
copies: number;
deviceName: string;
dpiHorizontal: number;
dpiVertical: number;
duplex: DuplexMode;
headerFooterEnabled: boolean;
landscape: boolean;
marginsType: MarginsType;
mediaSize: MediaSizeValue;
pagesPerSheet: number;
previewModifiable: boolean;
printerType: PrinterType;
rasterizePDF: boolean;
scaleFactor: number;
scalingType: ScalingType;
shouldPrintBackgrounds: boolean;
shouldPrintSelectionOnly: boolean;
advancedSettings?: object;
capabilities?: string;
marginsCustom?: MarginsSetting;
openPDFInPreview?: boolean;
pinValue?: string;
ticket?: string;
}
export type PrintTicket = Ticket&{
dpiDefault: boolean,
pageCount: number,
pageHeight: number,
pageWidth: number,
// <if expr="is_chromeos">
printToGoogleDrive: boolean,
// </if>
showSystemDialog: boolean,
};
/**
* Constant values matching printing::DuplexMode enum.
*/
export enum DuplexMode {
SIMPLEX = 0,
LONG_EDGE = 1,
SHORT_EDGE = 2,
UNKNOWN_DUPLEX_MODE = -1,
}
/**
* Values matching the types of duplex in a CDD.
*/
export enum DuplexType {
NO_DUPLEX = 'NO_DUPLEX',
LONG_EDGE = 'LONG_EDGE',
SHORT_EDGE = 'SHORT_EDGE',
}
let instance: PrintPreviewModelElement|null = null;
let whenReadyResolver: PromiseResolver<void> = new PromiseResolver();
export function getInstance(): PrintPreviewModelElement {
assert(instance);
return instance;
}
export function whenReady(): Promise<void> {
return whenReadyResolver.promise;
}
/**
* Sticky setting names in alphabetical order.
*/
const STICKY_SETTING_NAMES: string[] = [
'recentDestinations',
'collate',
'color',
'cssBackground',
'customMargins',
'dpi',
'duplex',
'duplexShortEdge',
'headerFooter',
'layout',
'margins',
'mediaSize',
'scaling',
'scalingType',
'scalingTypePdf',
'vendorItems',
];
// <if expr="is_chromeos">
STICKY_SETTING_NAMES.push('pin', 'pinValue');
// </if>
/**
* Minimum height of page in microns to allow headers and footers. Should
* match the value for min_size_printer_units in printing/print_settings.cc
* so that we do not request header/footer for margins that will be zero.
*/
const MINIMUM_HEIGHT_MICRONS: number = 25400;
export class PrintPreviewModelElement extends PolymerElement {
static get is() {
return 'print-preview-model';
}
static get template() {
return null;
}
static get properties() {
return {
/**
* Object containing current settings of Print Preview, for use by Polymer
* controls.
* Initialize all settings to available so that more settings always stays
* in a collapsed state during startup, when document information and
* printer capabilities may arrive at slightly different times.
*/
settings: {
type: Object,
notify: true,
value() {
return {
pages: {
value: [1],
unavailableValue: [],
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: false,
},
copies: {
value: 1,
unavailableValue: 1,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: false,
},
collate: {
value: true,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isCollateEnabled',
updatesPreview: false,
},
layout: {
value: false, /* portrait */
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isLandscapeEnabled',
updatesPreview: true,
},
color: {
value: true, /* color */
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isColorEnabled',
updatesPreview: true,
},
mediaSize: {
value: {},
unavailableValue: {
width_microns: 215900,
height_microns: 279400,
},
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'mediaSize',
updatesPreview: true,
},
margins: {
value: MarginsType.DEFAULT,
unavailableValue: MarginsType.DEFAULT,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'marginsType',
updatesPreview: true,
},
customMargins: {
value: {},
unavailableValue: {},
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'customMargins',
updatesPreview: true,
},
dpi: {
value: {},
unavailableValue: {},
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'dpi',
updatesPreview: false,
},
scaling: {
value: '100',
unavailableValue: '100',
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'scaling',
updatesPreview: true,
},
scalingType: {
value: ScalingType.DEFAULT,
unavailableValue: ScalingType.DEFAULT,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'scalingType',
updatesPreview: true,
},
scalingTypePdf: {
value: ScalingType.DEFAULT,
unavailableValue: ScalingType.DEFAULT,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'scalingTypePdf',
updatesPreview: true,
},
duplex: {
value: true,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isDuplexEnabled',
updatesPreview: false,
},
duplexShortEdge: {
value: false,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isDuplexShortEdge',
updatesPreview: false,
},
cssBackground: {
value: false,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isCssBackgroundEnabled',
updatesPreview: true,
},
selectionOnly: {
value: false,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: true,
},
headerFooter: {
value: true,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isHeaderFooterEnabled',
updatesPreview: true,
},
rasterize: {
value: false,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: true,
},
vendorItems: {
value: {},
unavailableValue: {},
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'vendorOptions',
updatesPreview: false,
},
pagesPerSheet: {
value: 1,
unavailableValue: 1,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: true,
},
// This does not represent a real setting value, and is used only to
// expose the availability of the other options settings section.
otherOptions: {
value: null,
unavailableValue: null,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: false,
},
// This does not represent a real settings value, but is used to
// propagate the correctly formatted ranges for print tickets.
ranges: {
value: [],
unavailableValue: [],
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: '',
updatesPreview: true,
},
recentDestinations: {
value: [],
unavailableValue: [],
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'recentDestinations',
updatesPreview: false,
},
// <if expr="is_chromeos">
pin: {
value: false,
unavailableValue: false,
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'isPinEnabled',
updatesPreview: false,
},
pinValue: {
value: '',
unavailableValue: '',
valid: true,
available: true,
setByPolicy: false,
setFromUi: false,
key: 'pinValue',
updatesPreview: false,
},
// </if>
};
},
},
settingsManaged: {
type: Boolean,
notify: true,
value: false,
},
destination: Object,
documentSettings: Object,
margins: Object,
pageSize: Object,
maxSheets: {
type: Number,
value: 0,
notify: true,
},
};
}
static get observers() {
return [
'updateSettingsFromDestination_(destination.capabilities)',
'updateSettingsAvailabilityFromDocumentSettings_(' +
'documentSettings.isModifiable, documentSettings.isFromArc,' +
'documentSettings.hasCssMediaStyles, documentSettings.hasSelection)',
'updateHeaderFooterAvailable_(' +
'margins, settings.margins.value, settings.mediaSize.value)',
];
}
settings: Settings;
settingsManaged: boolean;
destination: Destination;
documentSettings: DocumentSettings;
margins: Margins;
pageSize: Size;
maxSheets: number;
private initialized_: boolean = false;
private stickySettings_: SerializedSettings|null = null;
private policySettings_: PolicySettings|null = null;
private lastDestinationCapabilities_: Cdd|null = null;
override connectedCallback() {
super.connectedCallback();
assert(!instance);
instance = this;
whenReadyResolver.resolve();
}
override disconnectedCallback() {
super.disconnectedCallback();
instance = null;
whenReadyResolver = new PromiseResolver();
}
private fire_(eventName: string, detail?: any) {
this.dispatchEvent(
new CustomEvent(eventName, {bubbles: true, composed: true, detail}));
}
getSetting(settingName: string): Setting {
const setting = (this.get(settingName, this.settings) as Setting);
assert(setting, 'Setting is missing: ' + settingName);
return setting;
}
/**
* @param settingName Name of the setting to get the value for.
* @return The value of the setting, accounting for availability.
*/
getSettingValue(settingName: string): any {
const setting = this.getSetting(settingName);
return setting.available ? setting.value : setting.unavailableValue;
}
/**
* Updates settings.settingPath to |value|. Fires a preview-setting-changed
* event if the modification results in a change to the value returned by
* getSettingValue().
*/
private setSettingPath_(settingPath: string, value: any) {
const settingName = settingPath.split('.')[0];
const setting = this.getSetting(settingName);
const oldValue = this.getSettingValue(settingName);
this.set(`settings.${settingPath}`, value);
const newValue = this.getSettingValue(settingName);
if (newValue !== oldValue && setting.updatesPreview) {
this.fire_('preview-setting-changed');
}
}
/**
* Sets settings.settingName.value to |value|, unless updating the setting is
* disallowed by enterprise policy. Fires preview-setting-changed and
* sticky-setting-changed events if the update impacts the preview or requires
* an update to sticky settings. Used for setting settings from UI elements.
* @param settingName Name of the setting to set
* @param value The value to set the setting to.
* @param noSticky Whether to avoid stickying the setting. Defaults to false.
*/
setSetting(settingName: string, value: any, noSticky?: boolean) {
const setting = this.getSetting(settingName);
if (setting.setByPolicy) {
return;
}
const fireStickyEvent = !noSticky && setting.value !== value && setting.key;
this.setSettingPath_(`${settingName}.value`, value);
if (!noSticky) {
this.setSettingPath_(`${settingName}.setFromUi`, true);
}
if (fireStickyEvent && this.initialized_) {
this.fire_('sticky-setting-changed', this.getStickySettings_());
}
}
/**
* @param settingName Name of the setting to set
* @param start
* @param end
* @param newValue The value to add (if any).
* @param noSticky Whether to avoid stickying the setting. Defaults to false.
*/
setSettingSplice(
settingName: string, start: number, end: number, newValue: any,
noSticky?: boolean) {
const setting = this.getSetting(settingName);
if (setting.setByPolicy) {
return;
}
if (newValue) {
this.splice(`settings.${settingName}.value`, start, end, newValue);
} else {
this.splice(`settings.${settingName}.value`, start, end);
}
if (!noSticky) {
this.setSettingPath_(`${settingName}.setFromUi`, true);
}
if (!noSticky && setting.key && this.initialized_) {
this.fire_('sticky-setting-changed', this.getStickySettings_());
}
}
/**
* Sets the validity of |settingName| to |valid|. If the validity is changed,
* fires a setting-valid-changed event.
* @param settingName Name of the setting to set
* @param valid Whether the setting value is currently valid.
*/
setSettingValid(settingName: string, valid: boolean) {
const setting = this.getSetting(settingName);
// Should not set the setting to invalid if it is not available, as there
// is no way for the user to change the value in this case.
if (!valid) {
assert(setting.available, 'Setting is not available: ' + settingName);
}
const shouldFireEvent = valid !== setting.valid;
this.set(`settings.${settingName}.valid`, valid);
if (shouldFireEvent) {
this.fire_('setting-valid-changed', valid);
}
}
/**
* Updates the availability of the settings sections and values of dpi and
* media size settings based on the destination capabilities.
*/
private updateSettingsFromDestination_() {
if (!this.destination || !this.settings) {
return;
}
if (this.destination.capabilities === this.lastDestinationCapabilities_) {
return;
}
this.lastDestinationCapabilities_ = this.destination.capabilities;
const caps = this.destination.capabilities ?
this.destination.capabilities.printer :
null;
this.updateSettingsAvailabilityFromDestination_(caps);
if (!caps) {
return;
}
this.updateSettingsValues_(caps);
this.applyPersistentCddDefaults_();
}
private updateSettingsAvailabilityFromDestination_(caps: CddCapabilities|
null) {
this.setSettingPath_(
'copies.available', this.destination.hasCopiesCapability);
this.setSettingPath_('collate.available', !!caps && !!caps.collate);
this.setSettingPath_(
'color.available', this.destination.hasColorCapability);
const capsHasDuplex = !!caps && !!caps.duplex && !!caps.duplex.option;
const capsHasLongEdge = capsHasDuplex &&
caps!.duplex!.option.some(o => o.type === DuplexType.LONG_EDGE);
const capsHasShortEdge = capsHasDuplex &&
caps!.duplex!.option.some(o => o.type === DuplexType.SHORT_EDGE);
this.setSettingPath_(
'duplexShortEdge.available', capsHasLongEdge && capsHasShortEdge);
this.setSettingPath_(
'duplex.available',
(capsHasLongEdge || capsHasShortEdge) &&
caps!.duplex!.option.some(o => o.type === DuplexType.NO_DUPLEX));
this.setSettingPath_(
'vendorItems.available', !!caps && !!caps.vendor_capability);
// <if expr="is_chromeos">
const pinSupported = !!caps && !!caps.pin && !!caps.pin.supported &&
loadTimeData.getBoolean('isEnterpriseManaged');
this.set('settings.pin.available', pinSupported);
this.set('settings.pinValue.available', pinSupported);
// </if>
if (this.documentSettings) {
this.updateSettingsAvailabilityFromDestinationAndDocumentSettings_();
}
}
private updateSettingsAvailabilityFromDestinationAndDocumentSettings_() {
const isSaveAsPDF = this.destination.type === PrinterType.PDF_PRINTER;
const knownSizeToSaveAsPdf = isSaveAsPDF &&
(!this.documentSettings.isModifiable ||
this.documentSettings.hasCssMediaStyles);
const scalingAvailable =
!knownSizeToSaveAsPdf && !this.documentSettings.isFromArc;
this.setSettingPath_('scaling.available', scalingAvailable);
this.setSettingPath_(
'scalingType.available',
scalingAvailable && this.documentSettings.isModifiable);
this.setSettingPath_(
'scalingTypePdf.available',
scalingAvailable && !this.documentSettings.isModifiable);
const caps = this.destination && this.destination.capabilities ?
this.destination.capabilities.printer :
null;
this.setSettingPath_(
'mediaSize.available',
!!caps && !!caps.media_size && !knownSizeToSaveAsPdf);
this.setSettingPath_(
'dpi.available',
!this.documentSettings.isFromArc && !!caps && !!caps.dpi &&
!!caps.dpi.option && caps.dpi.option.length > 1);
this.setSettingPath_('layout.available', this.isLayoutAvailable_(caps));
}
private updateSettingsAvailabilityFromDocumentSettings_() {
if (!this.settings) {
return;
}
this.setSettingPath_(
'pagesPerSheet.available', !this.documentSettings.isFromArc);
this.setSettingPath_(
'margins.available',
!this.documentSettings.isFromArc && this.documentSettings.isModifiable);
this.setSettingPath_(
'customMargins.available',
!this.documentSettings.isFromArc && this.documentSettings.isModifiable);
this.setSettingPath_(
'cssBackground.available',
!this.documentSettings.isFromArc && this.documentSettings.isModifiable);
this.setSettingPath_(
'selectionOnly.available',
!this.documentSettings.isFromArc &&
this.documentSettings.isModifiable &&
this.documentSettings.hasSelection);
this.setSettingPath_(
'headerFooter.available',
!this.documentSettings.isFromArc && this.isHeaderFooterAvailable_());
this.setSettingPath_(
'rasterize.available',
!this.documentSettings.isFromArc && this.isRasterizeAvailable_());
this.setSettingPath_(
'otherOptions.available',
this.settings.cssBackground.available ||
this.settings.selectionOnly.available ||
this.settings.headerFooter.available ||
this.settings.rasterize.available);
if (this.destination) {
this.updateSettingsAvailabilityFromDestinationAndDocumentSettings_();
}
}
private updateHeaderFooterAvailable_() {
if (this.documentSettings === undefined) {
return;
}
this.setSettingPath_(
'headerFooter.available', this.isHeaderFooterAvailable_());
}
/**
* @return Whether the header/footer setting should be available.
*/
private isHeaderFooterAvailable_(): boolean {
// Always unavailable for PDFs.
if (!this.documentSettings.isModifiable) {
return false;
}
// Always unavailable for small paper sizes.
const microns = this.getSettingValue('layout') ?
this.getSettingValue('mediaSize').width_microns :
this.getSettingValue('mediaSize').height_microns;
if (microns < MINIMUM_HEIGHT_MICRONS) {
return false;
}
// Otherwise, availability depends on the margins.
const marginsType = this.getSettingValue('margins') as MarginsType;
if (marginsType === MarginsType.NO_MARGINS) {
return false;
}
if (marginsType === MarginsType.MINIMUM) {
return true;
}
return !this.margins ||
this.margins.get(CustomMarginsOrientation.TOP) > 0 ||
this.margins.get(CustomMarginsOrientation.BOTTOM) > 0;
}
private updateRasterizeAvailable_() {
// Need document settings to know if source is PDF.
if (this.documentSettings === undefined) {
return;
}
this.setSettingPath_('rasterize.available', this.isRasterizeAvailable_());
}
/**
* @return Whether the rasterization setting should be available.
*/
private isRasterizeAvailable_(): boolean {
// Only a possibility for PDFs. Always available for PDFs on Linux and
// ChromeOS. crbug.com/675798
let available =
!!this.documentSettings && !this.documentSettings.isModifiable;
// <if expr="is_win or is_macosx">
// Availability on Windows or macOS depends upon policy.
if (!available || !this.policySettings_) {
return false;
}
const policy = this.policySettings_['printPdfAsImageAvailability'];
available = policy !== undefined && policy.value;
// </if>
return available;
}
private isLayoutAvailable_(caps: CddCapabilities|null): boolean {
if (!caps || !caps.page_orientation || !caps.page_orientation.option ||
(!this.documentSettings.isModifiable &&
!this.documentSettings.isFromArc) ||
this.documentSettings.hasCssMediaStyles) {
return false;
}
let hasAutoOrPortraitOption = false;
let hasLandscapeOption = false;
caps.page_orientation.option.forEach(option => {
hasAutoOrPortraitOption = hasAutoOrPortraitOption ||
option.type === 'AUTO' || option.type === 'PORTRAIT';
hasLandscapeOption = hasLandscapeOption || option.type === 'LANDSCAPE';
});
return hasLandscapeOption && hasAutoOrPortraitOption;
}
private updateSettingsValues_(caps: CddCapabilities|null) {
if (this.settings.mediaSize.available) {
const defaultOption =
caps!.media_size!.option.find(o => !!o.is_default) ||
caps!.media_size!.option[0];
let matchingOption = null;
// If the setting does not have a valid value, the UI has just started so
// do not try to get a matching value; just set the printer default in
// case the user doesn't have sticky settings.
if (this.settings.mediaSize.setFromUi) {
const currentMediaSize = this.getSettingValue('mediaSize');
matchingOption = caps!.media_size!.option.find(o => {
return o.height_microns === currentMediaSize.height_microns &&
o.width_microns === currentMediaSize.width_microns;
});
}
this.setSetting('mediaSize', matchingOption || defaultOption, true);
}
if (this.settings.dpi.available) {
const defaultOption =
caps!.dpi!.option.find(o => !!o.is_default) || caps!.dpi!.option[0];
let matchingOption = null;
if (this.settings.dpi.setFromUi) {
const currentDpi = this.getSettingValue('dpi');
matchingOption = caps!.dpi!.option.find(o => {
return o.horizontal_dpi === currentDpi.horizontal_dpi &&
o.vertical_dpi === currentDpi.vertical_dpi;
});
}
this.setSetting('dpi', matchingOption || defaultOption, true);
} else if (
caps && caps.dpi && caps.dpi.option && caps.dpi.option.length > 0) {
const unavailableValue =
caps!.dpi!.option.find(o => !!o.is_default) || caps!.dpi!.option[0];
this.setSettingPath_('dpi.unavailableValue', unavailableValue);
}
if (!this.settings.color.setFromUi && this.settings.color.available) {
const defaultOption = this.destination.defaultColorOption;
if (defaultOption) {
this.setSetting(
'color',
!['STANDARD_MONOCHROME', 'CUSTOM_MONOCHROME'].includes(
defaultOption.type!),
true);
}
} else if (
!this.settings.color.available && caps && caps.color &&
caps.color.option && caps.color.option.length > 0) {
this.setSettingPath_(
'color.unavailableValue',
!['STANDARD_MONOCHROME', 'CUSTOM_MONOCHROME'].includes(
caps.color.option[0].type!));
} else if (!this.settings.color.available) {
// if no color capability is reported, assume black and white.
this.setSettingPath_('color.unavailableValue', false);
}
if (!this.settings.duplex.setFromUi && this.settings.duplex.available) {
const defaultOption = caps!.duplex!.option.find(o => !!o.is_default);
this.setSetting(
'duplex',
defaultOption ? (defaultOption.type === DuplexType.LONG_EDGE ||
defaultOption.type === DuplexType.SHORT_EDGE) :
false,
true);
this.setSetting(
'duplexShortEdge',
defaultOption ? defaultOption.type === DuplexType.SHORT_EDGE : false,
true);
if (!this.settings.duplexShortEdge.available) {
// Duplex is available, so must have only one two sided printing option.
// Set duplexShortEdge's unavailable value based on the printer.
this.setSettingPath_(
'duplexShortEdge.unavailableValue',
caps!.duplex!.option.some(o => o.type === DuplexType.SHORT_EDGE));
}
} else if (
!this.settings.duplex.available && caps && caps.duplex &&
caps.duplex.option) {
// In this case, there must only be one option.
const hasLongEdge =
caps!.duplex!.option.some(o => o.type === DuplexType.LONG_EDGE);
const hasShortEdge =
caps!.duplex!.option.some(o => o.type === DuplexType.SHORT_EDGE);
// If the only option available is long edge, the value should always be
// true.
this.setSettingPath_(
'duplex.unavailableValue', hasLongEdge || hasShortEdge);
this.setSettingPath_('duplexShortEdge.unavailableValue', hasShortEdge);
} else if (!this.settings.duplex.available) {
// If no duplex capability is reported, assume false.
this.setSettingPath_('duplex.unavailableValue', false);
this.setSettingPath_('duplexShortEdge.unavailableValue', false);
}
if (this.settings.vendorItems.available) {
const vendorSettings: {[key: string]: any} = {};
for (const item of caps!.vendor_capability!) {
let defaultValue = null;
if (item.type === 'SELECT' && item.select_cap &&
item.select_cap.option) {
const defaultOption =
item.select_cap.option.find(o => !!o.is_default);
defaultValue = defaultOption ? defaultOption.value : null;
} else if (item.type === 'RANGE') {
if (item.range_cap) {
defaultValue = item.range_cap.default || null;
}
} else if (item.type === 'TYPED_VALUE') {
if (item.typed_value_cap) {
defaultValue = item.typed_value_cap.default || null;
}
}
if (defaultValue !== null) {
vendorSettings[item.id] = defaultValue;
}
}
this.setSetting('vendorItems', vendorSettings, true);
}
}
/**
* Caches the sticky settings and sets up the recent destinations. Sticky
* settings will be applied when destinaton capabilities have been retrieved.
*/
setStickySettings(savedSettingsStr: string|null) {
assert(!this.stickySettings_);
if (!savedSettingsStr) {
return;
}
let savedSettings;
try {
savedSettings = JSON.parse(savedSettingsStr) as SerializedSettings;
} catch (e) {
console.warn('Unable to parse state ' + e);
return; // use default values rather than updating.
}
if (savedSettings.version !== 2) {
return;
}
let recentDestinations = savedSettings.recentDestinations || [];
if (!Array.isArray(recentDestinations)) {
recentDestinations = [recentDestinations];
}
// Remove unsupported privet and cloud printers from the sticky settings,
// to free up these spots for supported printers.
const unsupportedOrigins: DestinationOrigin[] = [
DestinationOrigin.COOKIES,
// <if expr="is_chromeos">
DestinationOrigin.DEVICE,
// </if>
DestinationOrigin.PRIVET,
];
recentDestinations = recentDestinations.filter((d: RecentDestination) => {
return !unsupportedOrigins.includes(d.origin);
});
// Initialize recent destinations early so that the destination store can
// start trying to fetch them.
this.setSetting('recentDestinations', recentDestinations);
savedSettings.recentDestinations = recentDestinations;
this.stickySettings_ = savedSettings;
}
/**
* Helper function for configurePolicySetting_(). Sets value and managed flag
* for given setting.
* @param settingName Name of the setting being applied.
* @param value Value of the setting provided via policy.
* @param managed Flag showing whether value of setting is managed.
* @param applyOnDestinationUpdate Flag showing whether policy
* should be applied on every destination update.
*/
private setPolicySetting_(
settingName: string, value: any, managed: boolean,
applyOnDestinationUpdate: boolean) {
if (!this.policySettings_) {
this.policySettings_ = {};
}
(this.policySettings_ as {[key: string]: PolicyEntry})[settingName] = {
value: value,
managed: managed,
applyOnDestinationUpdate: applyOnDestinationUpdate,
};
}
/**
* Helper function for setPolicySettings(). Calculates value and managed flag
* of the setting according to allowed and default modes.
*/
private configurePolicySetting_(
settingName: string, allowedMode: any, defaultMode: any) {
switch (settingName) {
case 'headerFooter': {
const value = allowedMode !== undefined ? allowedMode : defaultMode;
if (value !== undefined) {
this.setPolicySetting_(
settingName, value, allowedMode !== undefined,
/*applyOnDestinationUpdate=*/ false);
}
break;
}
case 'cssBackground': {
const value = allowedMode ? allowedMode : defaultMode;
if (value !== undefined) {
this.setPolicySetting_(
settingName, value === BackgroundGraphicsModeRestriction.ENABLED,
!!allowedMode, /*applyOnDestinationUpdate=*/ false);
}
break;
}
case 'mediaSize': {
if (defaultMode !== undefined) {
this.setPolicySetting_(
settingName, defaultMode, /*managed=*/ false,
/*applyOnDestinationUpdate=*/ true);
}
break;
}
case 'color': {
const value = allowedMode ? allowedMode : defaultMode;
if (value !== undefined) {
this.setPolicySetting_(
settingName, value, !!allowedMode,
/*applyOnDestinationUpdate=*/ false);
}
break;
}
case 'duplex': {
const value = allowedMode ? allowedMode : defaultMode;
if (value !== undefined) {
this.setPolicySetting_(
settingName, value, !!allowedMode,
/*applyOnDestinationUpdate=*/ false);
}
break;
}
case 'pin': {
const value = allowedMode ? allowedMode : defaultMode;
if (value !== undefined) {
this.setPolicySetting_(
settingName, value, !!allowedMode,
/*applyOnDestinationUpdate=*/ false);
}
break;
}
case 'printPdfAsImageAvailability': {
const value = allowedMode !== undefined ? allowedMode : defaultMode;
if (value !== undefined) {
this.setPolicySetting_(
settingName, value, /*managed=*/ false,
/*applyOnDestinationUpdate=*/ false);
}
break;
}
case 'printPdfAsImage': {
if (defaultMode !== undefined) {
this.setPolicySetting_(
settingName, defaultMode, /*managed=*/ false,
/*applyOnDestinationUpdate=*/ false);
}
break;
}
default:
break;
}
}
/**
* Sets settings in accordance to policies from native code, and prevents
* those settings from being changed via other means.
*/
setPolicySettings(policies: Policies|undefined) {
if (policies === undefined) {
return;
}
const policiesObject = policies as {[key: string]: PolicyObjectEntry};
['headerFooter', 'cssBackground', 'mediaSize'].forEach(settingName => {
if (!policiesObject[settingName]) {
return;
}
const defaultMode = policiesObject[settingName].defaultMode;
const allowedMode = policiesObject[settingName].allowedMode;
this.configurePolicySetting_(settingName, allowedMode, defaultMode);
});
// <if expr="is_chromeos">
if (policiesObject['sheets']) {
if (!this.policySettings_) {
this.policySettings_ = {};
}
this.policySettings_['sheets'] = {
value: policiesObject['sheets'].value,
applyOnDestinationUpdate: false,
managed: true,
};
}
['color', 'duplex', 'pin'].forEach(settingName => {
if (!policiesObject[settingName]) {
return;
}
const defaultMode = policiesObject[settingName].defaultMode;
const allowedMode = policiesObject[settingName].allowedMode;
this.configurePolicySetting_(settingName, allowedMode, defaultMode);
});
// </if>
// <if expr="is_win or is_macosx">
if (policies['printPdfAsImageAvailability']) {
if (!this.policySettings_) {
this.policySettings_ = {};
}
const allowedMode = policies['printPdfAsImageAvailability'].allowedMode;
this.configurePolicySetting_(
'printPdfAsImageAvailability', allowedMode, /*defaultMode=*/ false);
}
// </if>
if (policies['printPdfAsImage']) {
if (!this.policySettings_) {
this.policySettings_ = {};
}
const defaultMode = policies['printPdfAsImage'].defaultMode;
this.configurePolicySetting_(
'printPdfAsImage', /*allowedMode=*/ undefined, defaultMode);
}
}
applyStickySettings() {
if (this.stickySettings_) {
STICKY_SETTING_NAMES.forEach(settingName => {
const setting = this.get(settingName, this.settings) as Setting;
const value =
(this.stickySettings_ as {[key: string]: any})[setting.key];
if (value !== undefined) {
this.setSetting(settingName, value);
} else {
this.applyScalingStickySettings_(settingName);
}
});
}
this.applyPersistentCddDefaults_();
this.applyPolicySettings_();
this.initialized_ = true;
this.updateManaged_();
this.stickySettings_ = null;
this.fire_('sticky-setting-changed', this.getStickySettings_());
}
/**
* Helper function for applyStickySettings(). Checks if the setting
* is a scaling setting and applies by applying the old types
* that rely on 'fitToPage' and 'customScaling'.
* @param settingName Name of the setting being applied.
*/
private applyScalingStickySettings_(settingName: string) {
// TODO(dhoss): Remove checks for 'customScaling' and 'fitToPage'
if (settingName === 'scalingType' &&
'customScaling' in this.stickySettings_!) {
const isCustom = this.stickySettings_['customScaling'];
const scalingType = isCustom ? ScalingType.CUSTOM : ScalingType.DEFAULT;
this.setSetting(settingName, scalingType);
} else if (settingName === 'scalingTypePdf') {
if ('isFitToPageEnabled' in this.stickySettings_!) {
const isFitToPage = this.stickySettings_['isFitToPageEnabled'];
const scalingTypePdf = isFitToPage ?
ScalingType.FIT_TO_PAGE :
this.getSetting('scalingType').value;
this.setSetting(settingName, scalingTypePdf);
} else if (this.getSetting('scalingType').value === ScalingType.CUSTOM) {
// In the event that 'isFitToPageEnabled' was not in the sticky
// settings, and 'scalingType' has been set to custom, we want
// 'scalingTypePdf' to match.
this.setSetting(settingName, ScalingType.CUSTOM);
}
}
}
private applyPolicySettings_() {
if (this.policySettings_) {
for (const [settingName, policy] of Object.entries(
this.policySettings_)) {
const policyEntry = policy as PolicyEntry;
// <if expr="is_chromeos">
if (settingName === 'sheets') {
this.maxSheets = policyEntry.value;
continue;
}
if (settingName === 'color') {
this.set(
'settings.color.value',
policyEntry.value === ColorModeRestriction.COLOR);
this.set('settings.color.setByPolicy', policyEntry.managed);
continue;
}
if (settingName === 'duplex') {
let setDuplexTypeByPolicy = false;
this.set(
'settings.duplex.value',
policyEntry.value !== DuplexModeRestriction.SIMPLEX);
if (policyEntry.value === DuplexModeRestriction.SHORT_EDGE) {
this.set('settings.duplexShortEdge.value', true);
setDuplexTypeByPolicy = true;
} else if (policyEntry.value === DuplexModeRestriction.LONG_EDGE) {
this.set('settings.duplexShortEdge.value', false);
setDuplexTypeByPolicy = true;
}
this.set('settings.duplex.setByPolicy', policyEntry.managed);
this.set(
'settings.duplexShortEdge.setByPolicy',
policyEntry.managed && setDuplexTypeByPolicy);
continue;
}
if (settingName === 'pin') {
if (policyEntry.value === PinModeRestriction.NO_PIN &&
policyEntry.managed) {
this.set('settings.pin.available', false);
this.set('settings.pinValue.available', false);
} else {
this.set(
'settings.pin.value',
policyEntry.value === PinModeRestriction.PIN);
}
this.set('settings.pin.setByPolicy', policyEntry.managed);
continue;
}
// </if>
// <if expr="is_win or is_macosx">
if (settingName === 'printPdfAsImageAvailability') {
this.updateRasterizeAvailable_();
if (this.settings.rasterize.available) {
// If rasterize is available then otherOptions must be available.
this.setSettingPath_('otherOptions.available', true);
}
continue;
}
// </if>
if (settingName === 'printPdfAsImage') {
if (policyEntry.value) {
this.setSetting('rasterize', policyEntry.value, true);
}
continue;
}
if (policyEntry.value !== undefined &&
!policyEntry.applyOnDestinationUpdate) {
this.setSetting(settingName, policyEntry.value, true);
if (policyEntry.managed) {
this.set(`settings.${settingName}.setByPolicy`, true);
}
}
}
}
}
/**
* If the setting has a default value specified in the CDD capabilities and
* the attribute `reset_to_default` is true, this method will return the
* default value for the setting; otherwise it will return null.
*/
private getResetValue_(capability: CapabilityWithReset): (object|null) {
if (!capability.reset_to_default) {
return null;
}
const cddDefault = capability.option.find(o => !!o.is_default);
if (!cddDefault) {
return null;
}
return cddDefault;
}
/**
* For PrinterProvider printers, it's possible to specify for a setting to
* always reset to the default value using the `reset_to_default` attribute.
* If `reset_to_default` is true and a default value for the
* setting is specified, this method will reset the setting
* value to the default value.
*/
private applyPersistentCddDefaults_() {
if (!this.destination || !this.destination.isExtension) {
return;
}
const caps = this.destination && this.destination.capabilities ?
this.destination.capabilities.printer :
null;
if (!caps) {
return;
}
if (this.settings.mediaSize.available) {
const cddDefault = this.getResetValue_(caps['media_size']!);
if (cddDefault) {
this.set('settings.mediaSize.value', cddDefault);
}
}
if (this.settings.color.available) {
const cddDefault = this.getResetValue_(caps['color']!) as ColorOption;
if (cddDefault) {
this.set(
'settings.color.value',
!['STANDARD_MONOCHROME', 'CUSTOM_MONOCHROME'].includes(
cddDefault.type!));
}
}
if (this.settings.duplex.available) {
const cddDefault = this.getResetValue_(caps['duplex']!) as DuplexOption;
if (cddDefault) {
this.set(
'settings.duplex.value',
cddDefault.type === DuplexType.LONG_EDGE ||
cddDefault.type === DuplexType.SHORT_EDGE);
if (!this.settings.duplexShortEdge.available) {
this.set(
'settings.duplexShortEdge.value',
cddDefault.type === DuplexType.SHORT_EDGE);
}
}
}
if (this.settings.dpi.available) {
const cddDefault = this.getResetValue_(caps['dpi']!);
if (cddDefault) {
this.set('settings.dpi.value', cddDefault);
}
}
}
/**
* Restricts settings and applies defaults as defined by policy applicable to
* current destination.
*/
applyDestinationSpecificPolicies() {
if (this.settings.mediaSize.available && this.policySettings_) {
const mediaSizePolicy = this.policySettings_['mediaSize'] &&
this.policySettings_['mediaSize'].value;
if (mediaSizePolicy !== undefined) {
const matchingOption =
this.destination.capabilities!.printer.media_size!.option.find(
o => {
return o.width_microns === mediaSizePolicy.width &&
o.height_microns === mediaSizePolicy.height;
});
if (matchingOption !== undefined) {
this.set('settings.mediaSize.value', matchingOption);
}
}
}
this.updateManaged_();
}
private updateManaged_() {
let managedSettings = ['cssBackground', 'headerFooter'];
// <if expr="is_chromeos">
managedSettings =
managedSettings.concat(['color', 'duplex', 'duplexShortEdge', 'pin']);
// </if>
this.settingsManaged = managedSettings.some(settingName => {
const setting = this.getSetting(settingName);
return setting.available && setting.setByPolicy;
});
}
initialized(): boolean {
return this.initialized_;
}
private getStickySettings_(): string {
const serialization: {[key: string]: any} = {};
serialization['version'] = 2;
STICKY_SETTING_NAMES.forEach(settingName => {
const setting = this.get(settingName, this.settings);
if (setting.setFromUi) {
serialization[setting.key] = setting.value;
}
});
return JSON.stringify(serialization);
}
private getDuplexMode_(): DuplexMode {
if (!this.getSettingValue('duplex')) {
return DuplexMode.SIMPLEX;
}
return this.getSettingValue('duplexShortEdge') ? DuplexMode.SHORT_EDGE :
DuplexMode.LONG_EDGE;
}
private getCddDuplexType_(): DuplexType {
if (!this.getSettingValue('duplex')) {
return DuplexType.NO_DUPLEX;
}
return this.getSettingValue('duplexShortEdge') ? DuplexType.SHORT_EDGE :
DuplexType.LONG_EDGE;
}
/**
* Creates a string that represents a print ticket.
* @param destination Destination to print to.
* @param openPdfInPreview Whether this print request is to open
* the PDF in Preview app (Mac only).
* @param showSystemDialog Whether this print request is to show
* the system dialog.
* @return Serialized print ticket.
*/
createPrintTicket(
destination: Destination, openPdfInPreview: boolean,
showSystemDialog: boolean): string {
const dpi = this.getSettingValue('dpi') as DpiOption;
const scalingSettingKey = this.getSetting('scalingTypePdf').available ?
'scalingTypePdf' :
'scalingType';
const ticket: PrintTicket = {
mediaSize: this.getSettingValue('mediaSize') as MediaSizeValue,
pageCount: this.getSettingValue('pages').length,
landscape: this.getSettingValue('layout'),
color: destination.getNativeColorModel(
this.getSettingValue('color') as boolean),
headerFooterEnabled: false, // only used in print preview
marginsType: this.getSettingValue('margins'),
duplex: this.getDuplexMode_(),
copies: this.getSettingValue('copies'),
collate: this.getSettingValue('collate'),
shouldPrintBackgrounds: this.getSettingValue('cssBackground'),
shouldPrintSelectionOnly: false, // only used in print preview
previewModifiable: this.documentSettings.isModifiable,
printerType: destination.type,
rasterizePDF: this.getSettingValue('rasterize'),
scaleFactor:
this.getSettingValue(scalingSettingKey) === ScalingType.CUSTOM ?
parseInt(this.getSettingValue('scaling'), 10) :
100,
scalingType: this.getSettingValue(scalingSettingKey),
pagesPerSheet: this.getSettingValue('pagesPerSheet'),
dpiHorizontal: (dpi && 'horizontal_dpi' in dpi) ? dpi.horizontal_dpi : 0,
dpiVertical: (dpi && 'vertical_dpi' in dpi) ? dpi.vertical_dpi : 0,
dpiDefault: (dpi && 'is_default' in dpi) ? dpi.is_default! : false,
deviceName: destination.id,
pageWidth: this.pageSize.width,
pageHeight: this.pageSize.height,
showSystemDialog: showSystemDialog,
// <if expr="is_chromeos">
printToGoogleDrive:
destination.id === GooglePromotedDestinationId.SAVE_TO_DRIVE_CROS,
// </if>
};
if (openPdfInPreview) {
ticket['openPDFInPreview'] = openPdfInPreview;
}
if (this.getSettingValue('margins') === MarginsType.CUSTOM) {
ticket['marginsCustom'] = this.getSettingValue('customMargins');
}
if (destination.isExtension) {
// TODO(rbpotter): Get local and PDF printers to use the same ticket and
// send only this ticket instead of nesting it in a larger ticket.
ticket['ticket'] = this.createCloudJobTicket(destination);
ticket['capabilities'] = JSON.stringify(destination.capabilities);
}
// <if expr="is_chromeos">
if (this.getSettingValue('pin')) {
ticket['pinValue'] = this.getSettingValue('pinValue');
}
if (destination.origin === DestinationOrigin.CROS) {
ticket['advancedSettings'] = this.getSettingValue('vendorItems');
}
// </if>
return JSON.stringify(ticket);
}
/**
* Creates an object that represents a Google Cloud Print print ticket.
* @param destination Destination to print to.
* @return Google Cloud Print print ticket.
*/
createCloudJobTicket(destination: Destination): string {
assert(
destination.isExtension,
'Trying to create a Google Cloud Print print ticket for a local ' +
' non-extension destination');
assert(
destination.capabilities,
'Trying to create a Google Cloud Print print ticket for a ' +
'destination with no print capabilities');
// Create CJT (Cloud Job Ticket)
const cjt: CloudJobTicket = {version: '1.0', print: {}};
if (this.settings.collate.available) {
cjt.print.collate = {collate: this.settings.collate.value};
}
if (this.settings.color.available) {
const selectedOption = destination.getSelectedColorOption(
this.settings.color.value as boolean);
if (!selectedOption) {
console.warn('Could not find correct color option');
} else {
cjt.print.color = {type: selectedOption.type};
if (selectedOption.hasOwnProperty('vendor_id')) {
cjt.print.color!.vendor_id = selectedOption.vendor_id;
}
}
} else {
// Always try setting the color in the print ticket, otherwise a
// reasonable reader of the ticket will have to do more work, or process
// the ticket sub-optimally, in order to safely handle the lack of a
// color ticket item.
const defaultOption = destination.defaultColorOption;
if (defaultOption) {
cjt.print.color = {type: defaultOption.type};
if (defaultOption.hasOwnProperty('vendor_id')) {
cjt.print.color!.vendor_id = defaultOption.vendor_id;
}
}
}
if (this.settings.copies.available) {
cjt.print.copies = {copies: this.getSettingValue('copies')};
}
if (this.settings.duplex.available) {
cjt.print.duplex = {
type: this.getCddDuplexType_(),
};
}
if (this.settings.mediaSize.available) {
const mediaValue = this.settings.mediaSize.value;
cjt.print.media_size = {
width_microns: mediaValue.width_microns,
height_microns: mediaValue.height_microns,
is_continuous_feed: mediaValue.is_continuous_feed,
vendor_id: mediaValue.vendor_id,
};
}
if (!this.settings.layout.available) {
// In this case "orientation" option is hidden from user, so user can't
// adjust it for page content, see Landscape.isCapabilityAvailable().
// We can improve results if we set AUTO here.
const capability = destination.capabilities!.printer ?
destination.capabilities!.printer.page_orientation :
null;
if (capability && capability.option &&
capability.option.some(option => option.type === 'AUTO')) {
cjt.print.page_orientation = {type: 'AUTO'};
}
} else {
cjt.print.page_orientation = {
type: this.settings.layout.value ? 'LANDSCAPE' : 'PORTRAIT',
};
}
if (this.settings.dpi.available) {
const dpiValue = this.settings.dpi.value;
cjt.print.dpi = {
horizontal_dpi: dpiValue.horizontal_dpi,
vertical_dpi: dpiValue.vertical_dpi,
vendor_id: dpiValue.vendor_id,
};
}
if (this.settings.vendorItems.available) {
const items = this.settings.vendorItems.value;
cjt.print.vendor_ticket_item = [];
for (const itemId in items) {
if (items.hasOwnProperty(itemId)) {
cjt.print.vendor_ticket_item.push({id: itemId, value: items[itemId]});
}
}
}
return JSON.stringify(cjt);
}
}
declare global {
interface HTMLElementTagNameMap {
'print-preview-model': PrintPreviewModelElement;
}
}
customElements.define(PrintPreviewModelElement.is, PrintPreviewModelElement);