blob: 9c53c6a41d26cd1792c0f49c5ed7679e35f7d0ee [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 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/hidden_style_css.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
// <if expr="not is_chromeos">
import './destination_dialog.js';
// </if>
// <if expr="is_chromeos">
import './destination_dialog_cros.js';
// </if>
// <if expr="not is_chromeos">
import './destination_select.js';
// </if>
// <if expr="is_chromeos">
import './destination_select_cros.js';
// </if>
import './print_preview_shared.css.js';
import './print_preview_vars.css.js';
import './throbber.css.js';
import './settings_section.js';
import '../strings.m.js';
import {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.m.js';
import {I18nMixin} from 'chrome://resources/js/i18n_mixin.js';
import {WebUIListenerMixin} from 'chrome://resources/js/web_ui_listener_mixin.js';
import {beforeNextRender, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {createRecentDestinationKey, Destination, isPdfPrinter, makeRecentDestination, PrinterType, RecentDestination} from '../data/destination.js';
// <if expr="is_chromeos">
import {SAVE_TO_DRIVE_CROS_DESTINATION_KEY} from '../data/destination.js';
// </if>
import {DestinationErrorType, DestinationStore, DestinationStoreEventType} from '../data/destination_store.js';
import {Error, State} from '../data/state.js';
// <if expr="not is_chromeos">
import {PrintPreviewDestinationDialogElement} from './destination_dialog.js';
// </if>
// <if expr="is_chromeos">
import {PrintPreviewDestinationDialogCrosElement} from './destination_dialog_cros.js';
// </if>
// <if expr="not is_chromeos">
import {PrintPreviewDestinationSelectElement} from './destination_select.js';
// </if>
// <if expr="is_chromeos">
import {PrintPreviewDestinationSelectCrosElement} from './destination_select_cros.js';
// </if>
import {getTemplate} from './destination_settings.html.js';
import {SettingsMixin} from './settings_mixin.js';
export enum DestinationState {
INIT = 0,
SET = 1,
UPDATED = 2,
ERROR = 3,
}
/** Number of recent destinations to save. */
// <if expr="not is_chromeos">
export const NUM_PERSISTED_DESTINATIONS: number = 5;
// </if>
// <if expr="is_chromeos">
export const NUM_PERSISTED_DESTINATIONS: number = 10;
// </if>
/**
* Number of unpinned recent destinations to display.
* Pinned destinations include "Save as PDF" and "Save to Google Drive".
*/
const NUM_UNPINNED_DESTINATIONS: number = 3;
export interface PrintPreviewDestinationSettingsElement {
$: {
// <if expr="not is_chromeos">
destinationDialog:
CrLazyRenderElement<PrintPreviewDestinationDialogElement>,
destinationSelect: PrintPreviewDestinationSelectElement,
// </if>
// <if expr="is_chromeos">
destinationDialog:
CrLazyRenderElement<PrintPreviewDestinationDialogCrosElement>,
destinationSelect: PrintPreviewDestinationSelectCrosElement,
// </if>
};
}
const PrintPreviewDestinationSettingsElementBase =
I18nMixin(WebUIListenerMixin(SettingsMixin(PolymerElement)));
export class PrintPreviewDestinationSettingsElement extends
PrintPreviewDestinationSettingsElementBase {
static get is() {
return 'print-preview-destination-settings';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
dark: Boolean,
destination: {
type: Object,
notify: true,
value: null,
},
destinationState: {
type: Number,
notify: true,
value: DestinationState.INIT,
observer: 'updateDestinationSelect_',
},
disabled: Boolean,
error: {
type: Number,
notify: true,
observer: 'onErrorChanged_',
},
firstLoad: Boolean,
state: Number,
destinationStore_: {
type: Object,
value: null,
},
displayedDestinations_: Array,
// <if expr="is_chromeos">
driveDestinationKey_: {
type: String,
value: '',
},
hasPinSetting_: {
type: Boolean,
computed: 'computeHasPinSetting_(settings.pin.available)',
reflectToAttribute: true,
},
// </if>
isDialogOpen_: {
type: Boolean,
value: false,
},
noDestinations_: {
type: Boolean,
value: false,
},
pdfPrinterDisabled_: Boolean,
loaded_: {
type: Boolean,
computed: 'computeLoaded_(destinationState, destination)',
},
};
}
dark: boolean;
destination: Destination;
destinationState: DestinationState;
disabled: boolean;
error: Error;
firstLoad: boolean;
state: State;
private destinationStore_: DestinationStore|null;
private displayedDestinations_: Destination[];
// <if expr="is_chromeos">
private driveDestinationKey_: string;
private hasPinSetting_: boolean;
// </if>
private isDialogOpen_: boolean;
private noDestinations_: boolean;
private pdfPrinterDisabled_: boolean;
private loaded_: boolean;
private lastUser_: string = '';
private tracker_: EventTracker = new EventTracker();
override connectedCallback() {
super.connectedCallback();
this.destinationStore_ =
new DestinationStore(this.addWebUIListener.bind(this));
this.tracker_.add(
this.destinationStore_, DestinationStoreEventType.DESTINATION_SELECT,
this.onDestinationSelect_.bind(this));
this.tracker_.add(
this.destinationStore_,
DestinationStoreEventType.SELECTED_DESTINATION_CAPABILITIES_READY,
this.onDestinationCapabilitiesReady_.bind(this));
this.tracker_.add(
this.destinationStore_, DestinationStoreEventType.ERROR,
this.onDestinationError_.bind(this));
// Need to update the recent list when the destination store inserts
// destinations, in case any recent destinations have been added to the
// store. At startup, recent destinations can be in the sticky settings,
// but they should not be displayed in the dropdown until they have been
// fetched by the DestinationStore, to ensure that they still exist.
this.tracker_.add(
this.destinationStore_, DestinationStoreEventType.DESTINATIONS_INSERTED,
this.updateDropdownDestinations_.bind(this));
// <if expr="is_chromeos">
this.tracker_.add(
this.destinationStore_,
DestinationStoreEventType.DESTINATION_EULA_READY,
this.updateDestinationEulaUrl_.bind(this));
// </if>
}
override disconnectedCallback() {
super.disconnectedCallback();
this.destinationStore_!.resetTracker();
this.tracker_.removeAll();
}
/**
* @param defaultPrinter The system default printer ID.
* @param pdfPrinterDisabled Whether the PDF printer is disabled.
* @param isDriveMounted Whether Google Drive is mounted. Only used
on Chrome OS.
* @param serializedDefaultDestinationRulesStr String with rules
* for selecting a default destination.
*/
init(
defaultPrinter: string, pdfPrinterDisabled: boolean,
isDriveMounted: boolean,
serializedDefaultDestinationRulesStr: string|null) {
this.pdfPrinterDisabled_ = pdfPrinterDisabled;
let recentDestinations =
this.getSettingValue('recentDestinations') as RecentDestination[];
// <if expr="is_chromeos">
this.driveDestinationKey_ =
isDriveMounted ? SAVE_TO_DRIVE_CROS_DESTINATION_KEY : '';
// </if>
recentDestinations = recentDestinations.slice(
0, this.getRecentDestinationsDisplayCount_(recentDestinations));
this.destinationStore_!.init(
this.pdfPrinterDisabled_, isDriveMounted, defaultPrinter,
serializedDefaultDestinationRulesStr, recentDestinations);
}
/**
* @param recentDestinations recent destinations.
* @return Number of recent destinations to display.
*/
private getRecentDestinationsDisplayCount_(recentDestinations:
RecentDestination[]): number {
let numDestinationsToDisplay = NUM_UNPINNED_DESTINATIONS;
for (let i = 0; i < recentDestinations.length; i++) {
// Once all NUM_UNPINNED_DESTINATIONS unpinned destinations have been
// found plus an extra unpinned destination, return the total number of
// destinations found excluding the last extra unpinned destination.
//
// The extra unpinned destination ensures that pinned destinations
// located directly after the last unpinned destination are included
// in the display count.
if (i > numDestinationsToDisplay) {
return numDestinationsToDisplay;
}
// If a destination is pinned, increment numDestinationsToDisplay.
if (isPdfPrinter(recentDestinations[i].id)) {
numDestinationsToDisplay++;
}
}
return Math.min(recentDestinations.length, numDestinationsToDisplay);
}
private onDestinationSelect_() {
if (this.state === State.FATAL_ERROR) {
// Don't let anything reset if there is a fatal error.
return;
}
const destination = this.destinationStore_!.selectedDestination!;
this.destinationState = DestinationState.SET;
// Notify observers that the destination is set only after updating the
// destinationState.
this.destination = destination;
this.updateRecentDestinations_();
}
private onDestinationCapabilitiesReady_() {
this.notifyPath('destination.capabilities');
this.updateRecentDestinations_();
if (this.destinationState === DestinationState.SET) {
this.destinationState = DestinationState.UPDATED;
}
}
private onDestinationError_(e: CustomEvent<DestinationErrorType>) {
let errorType = Error.NONE;
switch (e.detail) {
case DestinationErrorType.INVALID:
errorType = Error.INVALID_PRINTER;
break;
case DestinationErrorType.NO_DESTINATIONS:
errorType = Error.NO_DESTINATIONS;
this.noDestinations_ = true;
break;
default:
break;
}
this.error = errorType;
}
private onErrorChanged_() {
if (this.error === Error.INVALID_PRINTER ||
this.error === Error.NO_DESTINATIONS) {
this.destinationState = DestinationState.ERROR;
}
}
private updateRecentDestinations_() {
if (!this.destination) {
return;
}
// Determine if this destination is already in the recent destinations,
// where in the array it is located, and whether or not it is visible.
const newDestination = makeRecentDestination(this.destination);
const recentDestinations =
this.getSettingValue('recentDestinations') as RecentDestination[];
let indexFound = -1;
// Note: isVisible should be only be used if the destination is unpinned.
// Although pinned destinations are always visible, isVisible may not
// necessarily be set to true in this case.
let isVisible = false;
let numUnpinnedChecked = 0;
for (let index = 0; index < recentDestinations.length; index++) {
const recent = recentDestinations[index];
if (recent.id === newDestination.id &&
recent.origin === newDestination.origin) {
indexFound = index;
// If we haven't seen the maximum unpinned destinations already, this
// destination is visible in the dropdown.
isVisible = numUnpinnedChecked < NUM_UNPINNED_DESTINATIONS;
break;
}
if (!isPdfPrinter(recent.id)) {
numUnpinnedChecked++;
}
}
// No change
if (indexFound === 0 &&
recentDestinations[0].capabilities === newDestination.capabilities) {
return;
}
const isNew = indexFound === -1;
// Shift the array so that the nth most recent destination is located at
// index n.
if (isNew && recentDestinations.length === NUM_PERSISTED_DESTINATIONS) {
indexFound = NUM_PERSISTED_DESTINATIONS - 1;
}
if (indexFound !== -1) {
this.setSettingSplice('recentDestinations', indexFound, 1, null);
}
// Add the most recent destination
this.setSettingSplice('recentDestinations', 0, 0, newDestination);
// The dropdown needs to be updated if a new printer or one not currently
// visible in the dropdown has been added.
if (!isPdfPrinter(newDestination.id) && (isNew || !isVisible)) {
this.updateDropdownDestinations_();
}
}
private updateDropdownDestinations_() {
const recentDestinations =
this.getSettingValue('recentDestinations') as RecentDestination[];
const updatedDestinations: Destination[] = [];
let numDestinationsChecked = 0;
for (const recent of recentDestinations) {
if (isPdfPrinter(recent.id)) {
continue;
}
numDestinationsChecked++;
const key = createRecentDestinationKey(recent);
const destination = this.destinationStore_!.getDestinationByKey(key);
if (destination) {
updatedDestinations.push(destination);
}
if (numDestinationsChecked === NUM_UNPINNED_DESTINATIONS) {
break;
}
}
this.displayedDestinations_ = updatedDestinations;
}
/**
* @return Whether the destinations dropdown should be disabled.
*/
private shouldDisableDropdown_(): boolean {
return this.state === State.FATAL_ERROR ||
(this.destinationState === DestinationState.UPDATED && this.disabled &&
this.state !== State.NOT_READY);
}
private computeLoaded_(): boolean {
return this.destinationState === DestinationState.ERROR ||
this.destinationState === DestinationState.UPDATED ||
(this.destinationState === DestinationState.SET && !!this.destination &&
(!!this.destination.capabilities ||
this.destination.type === PrinterType.PDF_PRINTER));
}
// <if expr="is_chromeos">
private computeHasPinSetting_(): boolean {
return this.getSetting('pin').available;
}
// </if>
/**
* @param e Event containing the key of the recent destination that was
* selected, or "seeMore".
*/
private onSelectedDestinationOptionChange_(e: CustomEvent<string>) {
const value = e.detail;
if (value === 'seeMore') {
this.destinationStore_!.startLoadAllDestinations();
this.$.destinationDialog.get().show();
this.isDialogOpen_ = true;
} else {
this.destinationStore_!.selectDestinationByKey(value);
}
}
private onDialogClose_() {
// Reset the select value if the user dismissed the dialog without
// selecting a new destination.
this.updateDestinationSelect_();
this.isDialogOpen_ = false;
}
private updateDestinationSelect_() {
if (this.destinationState === DestinationState.ERROR && !this.destination) {
return;
}
if (this.destinationState === DestinationState.INIT) {
return;
}
const shouldFocus =
this.destinationState !== DestinationState.SET && !this.firstLoad;
beforeNextRender(this.$.destinationSelect, () => {
this.$.destinationSelect.updateDestination();
if (shouldFocus) {
this.$.destinationSelect.focus();
}
});
}
getDestinationStoreForTest(): DestinationStore {
assert(this.destinationStore_);
return this.destinationStore_;
}
// <if expr="is_chromeos">
/**
* @param e Event containing the current destination's EULA URL.
*/
private updateDestinationEulaUrl_(e: CustomEvent<string>) {
if (!this.destination) {
return;
}
this.destination.eulaUrl = e.detail;
this.notifyPath('destination.eulaUrl');
}
// </if>
}
declare global {
interface HTMLElementTagNameMap {
'print-preview-destination-settings':
PrintPreviewDestinationSettingsElement;
}
}
customElements.define(
PrintPreviewDestinationSettingsElement.is,
PrintPreviewDestinationSettingsElement);