blob: 0d79cc66b1c14a1d222c9269993fdec00a4c3304 [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 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render_lit.js';
import './destination_dialog.js';
import './destination_select.js';
import '/strings.m.js';
import type {CrLazyRenderLitElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render_lit.js';
import {I18nMixinLit} from 'chrome://resources/cr_elements/i18n_mixin_lit.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 {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, RecentDestination} from '../data/destination.js';
import {createRecentDestinationKey, isPdfPrinter, makeRecentDestination, PrinterType} from '../data/destination.js';
import {DestinationErrorType, DestinationStore, DestinationStoreEventType} from '../data/destination_store.js';
import {Error, State} from '../data/state.js';
import type {PrintPreviewDestinationDialogElement} from './destination_dialog.js';
import type {PrintPreviewDestinationSelectElement} from './destination_select.js';
import {getHtml} from './destination_settings.html.js';
import {getCss as getPrintPreviewSharedCss} from './print_preview_shared.css.js';
import {SettingsMixin} from './settings_mixin.js';
export enum DestinationState {
INIT = 0,
SET = 1,
UPDATED = 2,
ERROR = 3,
}
/** Number of recent destinations to save. */
export const NUM_PERSISTED_DESTINATIONS: number = 5;
/**
* 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 {
$: {
destinationDialog:
CrLazyRenderLitElement<PrintPreviewDestinationDialogElement>,
destinationSelect: PrintPreviewDestinationSelectElement,
};
}
const PrintPreviewDestinationSettingsElementBase =
I18nMixinLit(WebUiListenerMixinLit(SettingsMixin(CrLitElement)));
export class PrintPreviewDestinationSettingsElement extends
PrintPreviewDestinationSettingsElementBase {
static get is() {
return 'print-preview-destination-settings';
}
static override get styles() {
return [
getPrintPreviewSharedCss(),
];
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
dark: {type: Boolean},
destination: {
type: Object,
notify: true,
},
destinationState: {
type: Number,
notify: true,
},
disabled: {type: Boolean},
error: {
type: Number,
notify: true,
},
firstLoad: {type: Boolean},
state: {type: Number},
destinationStore_: {type: Object},
displayedDestinations_: {type: Array},
isDialogOpen_: {type: Boolean},
noDestinations_: {type: Boolean},
pdfPrinterDisabled_: {type: Boolean},
loaded_: {type: Boolean},
};
}
accessor dark: boolean = false;
accessor destination: Destination|null = null;
accessor destinationState: DestinationState = DestinationState.INIT;
accessor disabled: boolean = false;
accessor error: Error|null = null;
accessor firstLoad: boolean = false;
accessor state: State = State.NOT_READY;
protected accessor destinationStore_: DestinationStore|null = null;
protected accessor displayedDestinations_: Destination[] = [];
private accessor isDialogOpen_: boolean = false;
protected accessor noDestinations_: boolean = false;
protected accessor pdfPrinterDisabled_: boolean = false;
protected accessor loaded_: boolean = false;
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));
}
override disconnectedCallback() {
super.disconnectedCallback();
this.destinationStore_!.resetTracker();
this.tracker_.removeAll();
}
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has('error')) {
this.onErrorChanged_();
}
if (changedProperties.has('destinationState') ||
changedProperties.has('destination')) {
this.loaded_ = this.computeLoaded_();
}
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('destinationState')) {
this.updateDestinationSelect_();
}
}
/**
* @param defaultPrinter The system default printer ID.
* @param pdfPrinterDisabled Whether the PDF printer is disabled.
* @param serializedDefaultDestinationRulesStr String with rules
* for selecting a default destination.
*/
init(
defaultPrinter: string, pdfPrinterDisabled: boolean,
serializedDefaultDestinationRulesStr: string|null) {
this.pdfPrinterDisabled_ = pdfPrinterDisabled;
let recentDestinations =
this.getSettingValue('recentDestinations') as RecentDestination[];
recentDestinations = recentDestinations.slice(
0, this.getRecentDestinationsDisplayCount_(recentDestinations));
this.destinationStore_!.init(
this.pdfPrinterDisabled_, 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 async onDestinationCapabilitiesReady_() {
// Wait for any 'destination-changed' events to be fired first.
await this.updateComplete;
this.fire(
'destination-capabilities-changed',
this.destinationStore_!.selectedDestination);
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 i = 0; i < recentDestinations.length; i++) {
const recent = recentDestinations[i]!;
if (recent.id === newDestination.id &&
recent.origin === newDestination.origin) {
indexFound = i;
// 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;
}
// Create a clone first, otherwise array modifications will not be detected
// by the underlying Observable instance.
const recentDestinationsClone = recentDestinations.slice();
if (indexFound !== -1) {
// Remove from the list if it already exists, it will be re-added to the
// front below.
recentDestinationsClone.splice(indexFound, 1);
}
// Add the most recent destination
recentDestinationsClone.splice(0, 0, newDestination);
this.setSetting('recentDestinations', recentDestinationsClone);
// 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.
*/
protected 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));
}
/**
* @param e Event containing the key of the recent destination that was
* selected, or "seeMore".
*/
protected 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);
}
}
protected 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;
this.$.destinationSelect.updateDestination();
if (shouldFocus) {
this.$.destinationSelect.focus();
}
}
getDestinationStoreForTest(): DestinationStore {
assert(this.destinationStore_);
return this.destinationStore_;
}
}
export type DestinationSettingsElement = PrintPreviewDestinationSettingsElement;
declare global {
interface HTMLElementTagNameMap {
'print-preview-destination-settings':
PrintPreviewDestinationSettingsElement;
}
}
customElements.define(
PrintPreviewDestinationSettingsElement.is,
PrintPreviewDestinationSettingsElement);