// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview 'settings-cups-saved-printers' is a list container for Saved
* Printers.
*/
import 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import '../settings_shared.css.js';
import './cups_printer_types.js';
import './cups_printers_browser_proxy.js';
import './cups_printers_entry.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {addWebUiListener} from 'chrome://resources/js/cr.js';
import type {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {recordSettingChange} from '../metrics_recorder.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {matchesSearchTerm, sortPrinters} from './cups_printer_dialog_util.js';
import {PrinterListEntry} from './cups_printer_types.js';
import {PrinterSettingsUserAction, recordPrinterSettingsUserAction} from './cups_printers.js';
import {CupsPrinterInfo, CupsPrintersBrowserProxy, CupsPrintersBrowserProxyImpl} from './cups_printers_browser_proxy.js';
import {CupsPrintersEntryListMixin} from './cups_printers_entry_list_mixin.js';
import {getTemplate} from './cups_saved_printers.html.js';
import {getStatusReasonFromPrinterStatus, PrinterStatus, PrinterStatusReason} from './printer_status.js';
/**
* If the Show more button is visible, the minimum number of printers we show
* is 3.
*/
const MIN_VISIBLE_PRINTERS = 3;
/**
* Move a printer's position in |printerArr| from |fromIndex| to |toIndex|.
*/
function moveEntryInPrinters(
printerArr: PrinterListEntry[], fromIndex: number, toIndex: number): void {
const element = printerArr[fromIndex];
printerArr.splice(fromIndex, 1);
printerArr.splice(toIndex, 0, element);
}
const SettingsCupsSavedPrintersElementBase =
CupsPrintersEntryListMixin(WebUiListenerMixin(PolymerElement));
export class SettingsCupsSavedPrintersElement extends
SettingsCupsSavedPrintersElementBase {
static get is() {
return 'settings-cups-saved-printers';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* Search term for filtering |savedPrinters|.
*/
searchTerm: {
type: String,
value: '',
},
activePrinter: {
type: Object,
notify: true,
},
printersCount: {
type: Number,
computed: 'getFilteredPrintersLength_(filteredPrinters_.*)',
notify: true,
},
activePrinterListEntryIndex_: {
type: Number,
value: -1,
},
/**
* List of printers filtered through a search term.
*/
filteredPrinters_: {
type: Array,
value: () => [],
},
/**
* Array of new PrinterListEntry's that were added during this session.
*/
newPrinters_: {
type: Array,
value: () => [],
},
/**
* Keeps track of whether the user has tapped the Show more button. A
* search term will expand the collapsed list, so we need to keep track of
* whether the list expanded because of a search term or because the user
* tapped on the Show more button.
*/
hasShowMoreBeenTapped_: {
type: Boolean,
value: false,
},
/**
* Used by FocusRowBehavior to track the last focused element on a row.
*/
lastFocused_: Object,
/**
* Used by FocusRowBehavior to track if the list has been blurred.
*/
listBlurred_: Boolean,
/**
* The cache of printer status reasons. Used by printer status entries to
* look up their current printer status.
*/
printerStatusReasonCache_: {
type: Map<string, PrinterStatusReason>,
value() {
return new Map();
},
},
};
}
static get observers(): string[] {
return [
'onSearchOrPrintersChanged_(savedPrinters.*, searchTerm,' +
'hasShowMoreBeenTapped_, newPrinters_.*)',
'fetchAllPrinterStatuses_(savedPrinters.splices)',
];
}
activePrinter: CupsPrinterInfo|null;
printersCount: number;
searchTerm: string;
private activePrinterListEntryIndex_: number;
private browserProxy_: CupsPrintersBrowserProxy;
private filteredPrinters_: PrinterListEntry[];
private hasShowMoreBeenTapped_: boolean;
private lastFocused_: Object;
private listBlurred_: boolean;
private newPrinters_: PrinterListEntry[];
private visiblePrinterCounter_: number;
private printerStatusReasonCache_: Map<string, PrinterStatusReason>;
constructor() {
super();
this.browserProxy_ = CupsPrintersBrowserProxyImpl.getInstance();
// The number of printers we display if hidden printers are allowed.
// MIN_VISIBLE_PRINTERS is the default value and we never show fewer
// printers if the Show more button is visible.
this.visiblePrinterCounter_ = MIN_VISIBLE_PRINTERS;
addWebUiListener(
'local-printers-updated',
(printers: CupsPrinterInfo[]) => printers.forEach(
printer => this.onPrinterStatusReceived_(printer.printerStatus)));
}
override ready(): void {
super.ready();
this.addEventListener(
'open-action-menu',
(event: CustomEvent<{target: HTMLElement, item: PrinterListEntry}>) => {
this.onOpenActionMenu_(event);
});
}
/**
* Redoes the search whenever |searchTerm| or |savedPrinters| changes.
*/
private onSearchOrPrintersChanged_(): void {
if (!this.savedPrinters) {
return;
}
const updatedPrinters = this.getVisiblePrinters_();
this.updateList(
'filteredPrinters_',
(printer: PrinterListEntry) => printer.printerInfo.printerId,
updatedPrinters);
// Trigger a resize to display additional printers when the list size
// increases.
const printerEntryList =
this.shadowRoot!.querySelector<IronListElement>('#printerEntryList');
assert(printerEntryList);
printerEntryList.notifyResize();
}
private onOpenActionMenu_(
e: CustomEvent<{target: HTMLElement, item: PrinterListEntry}>): void {
const item = e.detail.item;
this.activePrinterListEntryIndex_ = this.savedPrinters.findIndex(
(printer: PrinterListEntry) =>
printer.printerInfo.printerId === item.printerInfo.printerId);
this.activePrinter =
this.get(['savedPrinters', this.activePrinterListEntryIndex_])
.printerInfo;
const target = e.detail.target;
this.shadowRoot!.querySelector('cr-action-menu')!.showAt(target);
}
private onEditClick_(): void {
// Event is caught by 'settings-cups-printers'.
const editCupsPrinterDetailsEvent =
new CustomEvent('edit-cups-printer-details', {
bubbles: true,
composed: true,
});
this.dispatchEvent(editCupsPrinterDetailsEvent);
this.closeActionMenu_();
recordPrinterSettingsUserAction(PrinterSettingsUserAction.EDIT_PRINTER);
}
private onRemoveClick_(): void {
// Remove this printer's current status reason from the cache so a stale
// status isn't shown if the printer is added back.
this.printerStatusReasonCache_.delete(this.activePrinter!.printerId);
this.browserProxy_.removeCupsPrinter(
this.activePrinter!.printerId, this.activePrinter!.printerName);
this.activePrinter = null;
this.activePrinterListEntryIndex_ = -1;
this.closeActionMenu_();
recordSettingChange(Setting.kRemovePrinter);
recordPrinterSettingsUserAction(PrinterSettingsUserAction.REMOVE_PRINTER);
}
private onShowMoreClick_(): void {
this.hasShowMoreBeenTapped_ = true;
}
/**
* Gets the printers to be shown in the UI. These printers are filtered
* by the search term, alphabetically sorted (if applicable), and are the
* printers not hidden by the Show more section.
*/
private getVisiblePrinters_(): PrinterListEntry[] {
// Filter printers through |searchTerm|. If |searchTerm| is empty,
// |filteredPrinters_| is just |savedPrinters|.
const updatedPrinters = this.searchTerm ?
this.savedPrinters.filter(
(item: PrinterListEntry) =>
matchesSearchTerm(item.printerInfo, this.searchTerm)) :
this.savedPrinters.slice();
updatedPrinters.sort(sortPrinters);
this.moveNewlyAddedPrinters_(updatedPrinters, 0 /* toIndex */);
if (this.shouldPrinterListBeCollapsed_()) {
// If the Show more button is visible, we only display the first
// N < |visiblePrinterCounter_| printers and the rest are hidden.
return updatedPrinters.filter(
(_: PrinterListEntry, idx: number) =>
idx < this.visiblePrinterCounter_);
}
return updatedPrinters;
}
private closeActionMenu_(): void {
this.shadowRoot!.querySelector('cr-action-menu')!.close();
}
/**
* @return Returns true if the no search message should be visible.
*/
private showNoSearchResultsMessage_(): boolean {
return !!this.searchTerm && !this.filteredPrinters_.length;
}
override onSavedPrintersAdded(addedPrinters: PrinterListEntry[]): void {
const currArr = this.newPrinters_.slice();
for (const printer of addedPrinters) {
this.visiblePrinterCounter_++;
currArr.push(printer);
}
this.set('newPrinters_', currArr);
}
override onSavedPrintersRemoved(removedPrinters: PrinterListEntry[]): void {
const currArr = this.newPrinters_.slice();
for (const printer of removedPrinters) {
const newPrinterRemovedIdx = currArr.findIndex(
p => p.printerInfo.printerId === printer.printerInfo.printerId);
// If the removed printer is a recently added printer, remove it from
// |currArr|.
if (newPrinterRemovedIdx > -1) {
currArr.splice(newPrinterRemovedIdx, 1);
}
this.visiblePrinterCounter_ =
Math.max(MIN_VISIBLE_PRINTERS, --this.visiblePrinterCounter_);
}
this.set('newPrinters_', currArr);
}
/**
* Keeps track of whether the Show more button should be visible which means
* that the printer list is collapsed. There are two ways a collapsed list
* may be expanded: the Show more button is tapped or if there is a search
* term.
* @return True if the printer list should be collapsed.
*/
private shouldPrinterListBeCollapsed_(): boolean {
// If |searchTerm| is set, never collapse the list.
if (this.searchTerm) {
return false;
}
// If |hasShowMoreBeenTapped_| is set to true, never collapse the list.
if (this.hasShowMoreBeenTapped_) {
return false;
}
// If the total number of saved printers does not exceed the number of
// visible printers, there is no need for the list to be collapsed.
if (this.savedPrinters.length - this.visiblePrinterCounter_ < 1) {
return false;
}
return true;
}
/**
* Moves printers that are in |newPrinters_| to position |toIndex| of
* |printerArr|. This moves all recently added printers to the top of the
* printer list.
*/
private moveNewlyAddedPrinters_(
printerArr: PrinterListEntry[], toIndex: number): void {
if (!this.newPrinters_.length) {
return;
}
// We have newly added printers, move them to the top of the list.
for (const printer of this.newPrinters_) {
const idx = printerArr.findIndex(
p => p.printerInfo.printerId === printer.printerInfo.printerId);
if (idx > -1) {
moveEntryInPrinters(printerArr, idx, toIndex);
}
}
}
private getFilteredPrintersLength_(): number {
return this.filteredPrinters_.length;
}
/** Query each saved printer for its printer status. */
private fetchAllPrinterStatuses_(): void {
this.savedPrinters.forEach(printer => {
this.fetchPrinterStatus_(printer.printerInfo.printerId);
});
}
/** Sends a printer status request for `printerId`. */
private fetchPrinterStatus_(printerId: string): void {
this.browserProxy_.requestPrinterStatusUpdate(printerId).then(
printerStatus => this.onPrinterStatusReceived_(printerStatus));
}
/**
* For each printer status received, add it to the printer status cache then
* notify its respective printer entry to update its status.
*/
private onPrinterStatusReceived_(printerStatus: PrinterStatus|
undefined): void {
if (!printerStatus?.printerId) {
return;
}
this.printerStatusReasonCache_.set(
printerStatus.printerId,
getStatusReasonFromPrinterStatus(printerStatus));
// The actual printer entries displayed are from `filteredPrinters_`. So
// notify the specific filtered printer entry to update its icon.
const filteredIndex = this.filteredPrinters_.findIndex(
printer => printer.printerInfo.printerId === printerStatus.printerId);
if (filteredIndex === -1) {
return;
}
this.notifyPath(`filteredPrinters_.${filteredIndex}.printerInfo.printerId`);
}
getPrinterStatusReasonCacheForTesting(): Map<string, PrinterStatusReason> {
return this.printerStatusReasonCache_;
}
}
declare global {
interface HTMLElementTagNameMap {
'settings-cups-saved-printers': SettingsCupsSavedPrintersElement;
}
interface HTMLElementEventMap {
'open-action-menu':
CustomEvent<{target: HTMLElement, item: PrinterListEntry}>;
}
}
customElements.define(
SettingsCupsSavedPrintersElement.is, SettingsCupsSavedPrintersElement);