UNPKG

chrome-devtools-frontend

Version:
738 lines (657 loc) • 29 kB
// Copyright 2013 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-lit-render-outside-of-view */ /* eslint-disable @devtools/no-imperative-dom-api */ import '../../ui/kit/kit.js'; import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Root from '../../core/root/root.js'; import * as GreenDev from '../../models/greendev/greendev.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as UIHelpers from '../../ui/helpers/helpers.js'; import {type Card, createIcon} from '../../ui/kit/kit.js'; import * as SettingsUI from '../../ui/legacy/components/settings_ui/settings_ui.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import {html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {PanelUtils} from '../utils/utils.js'; import * as PanelComponents from './components/components.js'; import type {KeybindsSettingsTab} from './KeybindsSettingsTab.js'; import settingsScreenStyles from './settingsScreen.css.js'; const UIStrings = { /** * @description Card header in Experiments settings tab that list all available unstable experiments that can be turned on or off. */ unstableExperiments: 'Unstable experiments', /** * @description Name of the Settings view */ settings: 'Settings', /** * @description Text for keyboard shortcuts */ shortcuts: 'Shortcuts', /** * @description Text of button in Settings Screen of the Settings */ restoreDefaultsAndReload: 'Restore defaults and reload', /** * @description Card header in Experiments settings tab that list all available stable experiments that can be turned on or off. */ experiments: 'Experiments', /** * @description Message shown in the experiments panel to warn users about any possible unstable features. */ theseExperimentsCouldBeUnstable: 'Warning: These experiments could be unstable or unreliable.', /** * @description Message shown in the GreenDev prototypes panel to warn users about any possible unstable features. */ greenDevUnstable: 'Warning: All these features are prototype and very unstable. They exist for user testing and are not designed to be relied on.', /** * @description Message text content in Settings Screen of the Settings */ theseExperimentsAreParticularly: 'Warning: These experiments are particularly unstable. Enable at your own risk.', /** * @description Message to display if a setting change requires a reload of DevTools */ oneOrMoreSettingsHaveChanged: 'One or more settings have changed which requires a reload to take effect', /** * @description Warning text shown when the user has entered text to filter the * list of experiments, but no experiments match the filter. */ noResults: 'No experiments match the filter', /** * @description Text that is usually a hyperlink to more documentation */ learnMore: 'Learn more', /** * @description Text that is usually a hyperlink to a feedback form */ sendFeedback: 'Send feedback', /** * @description Placeholder text in search bar */ searchExperiments: 'Search experiments', } as const; const str_ = i18n.i18n.registerUIStrings('panels/settings/SettingsScreen.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let settingsScreenInstance: SettingsScreen; function createSettingsCard(heading: Common.UIString.LocalizedString, ...content: HTMLElement[]): Card { const card = document.createElement('devtools-card'); card.heading = heading; card.append(...content); return card; } export class SettingsScreen extends UI.Widget.VBox implements UI.View.ViewLocationResolver { private readonly tabbedLocation: UI.View.TabbedViewLocation; private keybindsTab?: KeybindsSettingsTab; private reportTabOnReveal: boolean; private constructor() { super({useShadowDom: true}); this.registerRequiredCSS(settingsScreenStyles); this.contentElement.classList.add('settings-window-main'); this.contentElement.classList.add('vbox'); const settingsLabelElement = document.createElement('div'); settingsLabelElement.classList.add('settings-window-label-element'); const settingsTitleElement = UI.UIUtils.createShadowRootWithCoreStyles(settingsLabelElement, {cssFile: settingsScreenStyles}) .createChild('div', 'settings-window-title'); UI.ARIAUtils.markAsHeading(settingsTitleElement, 1); settingsTitleElement.textContent = i18nString(UIStrings.settings); this.tabbedLocation = UI.ViewManager.ViewManager.instance().createTabbedLocation( () => SettingsScreen.revealSettingsScreen(), 'settings-view'); const tabbedPane = this.tabbedLocation.tabbedPane(); tabbedPane.registerRequiredCSS(settingsScreenStyles); tabbedPane.headerElement().prepend(settingsLabelElement); tabbedPane.setShrinkableTabs(false); tabbedPane.makeVerticalTabLayout(); const keyBindsView = UI.ViewManager.ViewManager.instance().view('keybinds'); if (keyBindsView) { void keyBindsView.widget().then(widget => { this.keybindsTab = widget as KeybindsSettingsTab; }); } tabbedPane.show(this.contentElement); tabbedPane.selectTab('preferences'); tabbedPane.addEventListener(UI.TabbedPane.Events.TabInvoked, this.tabInvoked, this); this.reportTabOnReveal = false; } static instance(opts: {forceNew: boolean|null} = {forceNew: null}): SettingsScreen { const {forceNew} = opts; if (!settingsScreenInstance || forceNew) { settingsScreenInstance = new SettingsScreen(); } return settingsScreenInstance; } private static revealSettingsScreen(): SettingsScreen { const settingsScreen = SettingsScreen.instance(); if (settingsScreen.isShowing()) { return settingsScreen; } settingsScreen.reportTabOnReveal = true; const dialog = new UI.Dialog.Dialog('settings'); dialog.contentElement.removeAttribute('aria-modal'); dialog.contentElement.tabIndex = -1; dialog.addCloseButton(); dialog.setOutsideClickCallback(() => {}); dialog.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior.PIERCE_GLASS_PANE); dialog.setOutsideTabIndexBehavior(UI.Dialog.OutsideTabIndexBehavior.PRESERVE_MAIN_VIEW_TAB_INDEX); settingsScreen.show(dialog.contentElement); dialog.setEscapeKeyCallback(settingsScreen.onEscapeKeyPressed.bind(settingsScreen)); dialog.setMarginBehavior(UI.GlassPane.MarginBehavior.NO_MARGIN); dialog.show(); dialog.contentElement.focus(); return settingsScreen; } static async showSettingsScreen( options: ShowSettingsScreenOptions|undefined = {name: undefined, focusTabHeader: undefined}): Promise<void> { const {name, focusTabHeader} = options; const settingsScreen = SettingsScreen.revealSettingsScreen(); settingsScreen.selectTab(name || 'preferences'); const tabbedPane = settingsScreen.tabbedLocation.tabbedPane(); await tabbedPane.waitForTabElementUpdate(); if (focusTabHeader) { tabbedPane.focusSelectedTabHeader(); } else { tabbedPane.focus(); } } resolveLocation(_locationName: string): UI.View.ViewLocation|null { return this.tabbedLocation; } private selectTab(name: string): void { this.tabbedLocation.tabbedPane().selectTab(name, /* userGesture */ true); } private tabInvoked(event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void { const eventData = event.data; if (!eventData.isUserGesture) { return; } const prevTabId = eventData.prevTabId; const tabId = eventData.tabId; if (!this.reportTabOnReveal && prevTabId && prevTabId === tabId) { return; } this.reportTabOnReveal = false; this.reportSettingsPanelShown(tabId); } private reportSettingsPanelShown(tabId: string): void { if (tabId === i18nString(UIStrings.shortcuts)) { Host.userMetrics.settingsPanelShown('shortcuts'); return; } Host.userMetrics.settingsPanelShown(tabId); } private onEscapeKeyPressed(event: KeyboardEvent): void { if (this.tabbedLocation.tabbedPane().selectedTabId === 'keybinds' && this.keybindsTab) { this.keybindsTab.onEscapeKeyPressed(event); } } } interface SettingsTab { highlightObject(object: Object): void; } export class GenericSettingsTab extends UI.Widget.VBox implements SettingsTab { private readonly syncSection = new PanelComponents.SyncSection.SyncSection(); private readonly settingToControl = new Map<Common.Settings.Setting<unknown>, HTMLElement>(); private readonly containerElement: HTMLElement; #updateSyncSectionTimerId = -1; #syncSectionUpdatePromise: Promise<void>|null = null; constructor() { super({jslog: `${VisualLogging.pane('preferences')}`}); this.element.classList.add('settings-tab-container'); this.element.id = 'preferences-tab-content'; this.containerElement = this.contentElement.createChild('div', 'settings-card-container-wrapper').createChild('div'); this.containerElement.classList.add('settings-multicolumn-card-container'); this.syncSection.markAsRoot(); // AI, GRID, MOBILE, EMULATION, and RENDERING are intentionally excluded from this list. // AI settings are displayed in their own tab. const explicitSectionOrder: Common.Settings.SettingCategory[] = [ Common.Settings.SettingCategory.NONE, Common.Settings.SettingCategory.APPEARANCE, Common.Settings.SettingCategory.SOURCES, Common.Settings.SettingCategory.ELEMENTS, Common.Settings.SettingCategory.NETWORK, Common.Settings.SettingCategory.PERFORMANCE, Common.Settings.SettingCategory.MEMORY, Common.Settings.SettingCategory.CONSOLE, Common.Settings.SettingCategory.EXTENSIONS, Common.Settings.SettingCategory.PERSISTENCE, Common.Settings.SettingCategory.DEBUGGER, Common.Settings.SettingCategory.GLOBAL, Common.Settings.SettingCategory.ACCOUNT, ]; // Some settings define their initial ordering. const preRegisteredSettings = Common.Settings.Settings.instance().getRegisteredSettings().sort( (firstSetting, secondSetting) => { if (firstSetting.order && secondSetting.order) { return (firstSetting.order - secondSetting.order); } if (firstSetting.order) { return -1; } if (secondSetting.order) { return 1; } return 0; }, ); for (const sectionCategory of explicitSectionOrder) { const settingsForSection = preRegisteredSettings.filter( setting => setting.category === sectionCategory && GenericSettingsTab.isSettingVisible(setting)); this.createSectionElement(sectionCategory, settingsForSection); } const restoreAndReloadButton = UI.UIUtils.createTextButton( i18nString(UIStrings.restoreDefaultsAndReload), restoreAndReload, {jslogContext: 'settings.restore-defaults-and-reload'}); this.containerElement.appendChild(restoreAndReloadButton); function restoreAndReload(): void { Common.Settings.Settings.instance().clearAll(); Components.Reload.reload(); } } static isSettingVisible(setting: Common.Settings.SettingRegistration): boolean { return Boolean(setting.title?.()) && Boolean(setting.category); } override wasShown(): void { UI.Context.Context.instance().setFlavor(GenericSettingsTab, this); super.wasShown(); this.updateSyncSection(); } override willHide(): void { if (this.#updateSyncSectionTimerId > 0) { window.clearTimeout(this.#updateSyncSectionTimerId); this.#updateSyncSectionTimerId = -1; } super.willHide(); UI.Context.Context.instance().setFlavor(GenericSettingsTab, null); } private updateSyncSection(): void { if (this.#updateSyncSectionTimerId > 0) { window.clearTimeout(this.#updateSyncSectionTimerId); this.#updateSyncSectionTimerId = -1; } this.#syncSectionUpdatePromise = new Promise<Host.InspectorFrontendHostAPI.SyncInformation>( resolve => Host.InspectorFrontendHost.InspectorFrontendHostInstance.getSyncInformation(resolve)) .then(syncInfo => { this.syncSection.syncInfo = syncInfo; if (!syncInfo.isSyncActive || !syncInfo.arePreferencesSynced) { this.#updateSyncSectionTimerId = window.setTimeout(this.updateSyncSection.bind(this), 500); } }); } private createExtensionSection(settings: Common.Settings.SettingRegistration[]): void { const sectionName = Common.Settings.SettingCategory.EXTENSIONS; const settingUI = Components.Linkifier.LinkHandlerSettingUI.instance(); const element = settingUI.settingElement(); this.createStandardSectionElement(sectionName, settings, element); } private createSectionElement( category: Common.Settings.SettingCategory, settings: Common.Settings.SettingRegistration[]): void { // Always create the EXTENSIONS section and append the link handling control. if (category === Common.Settings.SettingCategory.EXTENSIONS) { this.createExtensionSection(settings); } else if (category === Common.Settings.SettingCategory.ACCOUNT && settings.length > 0) { const syncCard = createSettingsCard( Common.SettingRegistration.getLocalizedSettingsCategory(Common.SettingRegistration.SettingCategory.ACCOUNT), this.syncSection.element); this.containerElement.appendChild(syncCard); } else if (settings.length > 0) { this.createStandardSectionElement(category, settings); } } private createStandardSectionElement( category: Common.Settings.SettingCategory, settings: Common.Settings.SettingRegistration[], content?: Element): void { const uiSectionName = Common.Settings.getLocalizedSettingsCategory(category); const sectionElement = document.createElement('div'); for (const settingRegistration of settings) { const setting = Common.Settings.Settings.instance().moduleSetting(settingRegistration.settingName); const settingControl = SettingsUI.SettingsUI.createControlForSetting(setting); if (settingControl) { this.settingToControl.set(setting, settingControl); sectionElement.appendChild(settingControl); } } if (content) { sectionElement.appendChild(content); } const card = createSettingsCard(uiSectionName, sectionElement); this.containerElement.appendChild(card); } highlightObject(setting: Object): void { if (setting instanceof Common.Settings.Setting) { const element = this.settingToControl.get(setting); if (element) { PanelUtils.highlightElement(element); } else if (setting.name === 'receive-gdp-badges') { void this.#syncSectionUpdatePromise?.then(() => { void this.syncSection.highlightReceiveBadgesSetting(); }); } } } } export class ExperimentsSettingsTab extends UI.Widget.VBox implements SettingsTab { #experimentsSection: Card|undefined; #unstableExperimentsSection: Card|undefined; private readonly experimentToControl = new Map<Root.Runtime.Experiment, HTMLElement>(); private readonly containerElement: HTMLElement; constructor() { super({jslog: `${VisualLogging.pane('experiments')}`}); this.element.classList.add('settings-tab-container'); this.element.id = 'experiments-tab-content'; this.containerElement = this.contentElement.createChild('div', 'settings-card-container-wrapper').createChild('div'); this.containerElement.classList.add('settings-card-container'); const filterSection = this.containerElement.createChild('div'); filterSection.classList.add('experiments-filter'); render( html` <devtools-toolbar> <devtools-toolbar-input autofocus type="filter" placeholder=${ i18nString(UIStrings.searchExperiments)} style="flex-grow:1" @change=${ this.#onFilterChanged.bind(this)}></devtools-toolbar-input> </devtools-toolbar> `, filterSection); this.renderExperiments(''); } #onFilterChanged(e: CustomEvent<string>): void { this.renderExperiments(e.detail.toLowerCase()); } private renderExperiments(filterText: string): void { this.experimentToControl.clear(); if (this.#experimentsSection) { this.#experimentsSection.remove(); } if (this.#unstableExperimentsSection) { this.#unstableExperimentsSection.remove(); } const experiments = Root.Runtime.experiments.allConfigurableExperiments().sort(); const unstableExperiments = experiments.filter(e => e.unstable && e.title.toLowerCase().includes(filterText)); const stableExperiments = experiments.filter(e => !e.unstable && e.title.toLowerCase().includes(filterText)); if (stableExperiments.length) { const experimentsBlock = document.createElement('div'); experimentsBlock.classList.add('settings-experiments-block'); const warningMessage = i18nString(UIStrings.theseExperimentsCouldBeUnstable); const warningSection = this.createExperimentsWarningSubsection(warningMessage); for (const experiment of stableExperiments) { experimentsBlock.appendChild(this.createExperimentCheckbox(experiment)); } this.#experimentsSection = createSettingsCard(i18nString(UIStrings.experiments), warningSection, experimentsBlock); this.containerElement.appendChild(this.#experimentsSection); } if (unstableExperiments.length) { const experimentsBlock = document.createElement('div'); experimentsBlock.classList.add('settings-experiments-block'); const warningMessage = i18nString(UIStrings.theseExperimentsAreParticularly); for (const experiment of unstableExperiments) { experimentsBlock.appendChild(this.createExperimentCheckbox(experiment)); } this.#unstableExperimentsSection = createSettingsCard( i18nString(UIStrings.unstableExperiments), this.createExperimentsWarningSubsection(warningMessage), experimentsBlock); this.containerElement.appendChild(this.#unstableExperimentsSection); } if (!stableExperiments.length && !unstableExperiments.length) { const warning = document.createElement('span'); warning.textContent = i18nString(UIStrings.noResults); UI.ARIAUtils.LiveAnnouncer.alert(warning.textContent); this.#experimentsSection = createSettingsCard(i18nString(UIStrings.experiments), warning); this.containerElement.appendChild(this.#experimentsSection); } } private createExperimentsWarningSubsection(warningMessage: string): HTMLElement { const subsection = document.createElement('div'); subsection.classList.add('experiments-warning-subsection'); const warningIcon = createIcon('warning'); subsection.appendChild(warningIcon); const warning = subsection.createChild('span'); warning.textContent = warningMessage; return subsection; } private createExperimentCheckbox(experiment: Root.Runtime.Experiment): HTMLParagraphElement { const checkbox = UI.UIUtils.CheckboxLabel.createWithStringLiteral(experiment.title, experiment.isEnabled(), experiment.name); checkbox.classList.add('experiment-label'); checkbox.name = experiment.name; function listener(): void { experiment.setEnabled(checkbox.checked); Host.userMetrics.experimentChanged(experiment.name, experiment.isEnabled()); UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning( i18nString(UIStrings.oneOrMoreSettingsHaveChanged)); } checkbox.addEventListener('click', listener, false); const p = document.createElement('p'); this.experimentToControl.set(experiment, p); p.classList.add('settings-experiment'); if (experiment.unstable && !experiment.isEnabled()) { p.classList.add('settings-experiment-unstable'); } p.appendChild(checkbox); const experimentLink = experiment.docLink; if (experimentLink) { const linkButton = new Buttons.Button.Button(); linkButton.data = { iconName: 'help', variant: Buttons.Button.Variant.ICON, size: Buttons.Button.Size.SMALL, jslogContext: `${experiment.name}-documentation`, title: i18nString(UIStrings.learnMore), }; linkButton.addEventListener('click', () => UIHelpers.openInNewTab(experimentLink)); linkButton.classList.add('link-icon'); p.appendChild(linkButton); } if (experiment.feedbackLink) { const link = UI.XLink.XLink.create( experiment.feedbackLink, undefined, undefined, undefined, `${experiment.name}-feedback`); link.textContent = i18nString(UIStrings.sendFeedback); link.classList.add('feedback-link'); p.appendChild(link); } return p; } highlightObject(experiment: Object): void { if (experiment instanceof Root.Runtime.Experiment) { const element = this.experimentToControl.get(experiment); if (element) { PanelUtils.highlightElement(element); } } } override wasShown(): void { UI.Context.Context.instance().setFlavor(ExperimentsSettingsTab, this); super.wasShown(); } override willHide(): void { super.willHide(); UI.Context.Context.instance().setFlavor(ExperimentsSettingsTab, null); } } export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { handleAction(_context: UI.Context.Context, actionId: string): boolean { switch (actionId) { case 'settings.show': void SettingsScreen.showSettingsScreen({focusTabHeader: true} as ShowSettingsScreenOptions); return true; case 'settings.documentation': UIHelpers.openInNewTab('https://developer.chrome.com/docs/devtools/'); return true; case 'settings.shortcuts': void SettingsScreen.showSettingsScreen({name: 'keybinds', focusTabHeader: true}); return true; } return false; } } export class Revealer implements Common.Revealer.Revealer<Root.Runtime.Experiment|Common.Settings.Setting<unknown>> { async reveal(object: Root.Runtime.Experiment|Common.Settings.Setting<unknown>): Promise<void> { const context = UI.Context.Context.instance(); if (object instanceof Root.Runtime.Experiment) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront(); await SettingsScreen.showSettingsScreen({name: 'experiments'}); const experimentsSettingsTab = context.flavor(ExperimentsSettingsTab); if (experimentsSettingsTab !== null) { experimentsSettingsTab.highlightObject(object); } return; } for (const settingRegistration of Common.Settings.Settings.instance().getRegisteredSettings()) { if (!GenericSettingsTab.isSettingVisible(settingRegistration)) { continue; } if (settingRegistration.settingName === object.name) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront(); await SettingsScreen.showSettingsScreen(); const genericSettingsTab = context.flavor(GenericSettingsTab); if (genericSettingsTab !== null) { genericSettingsTab.highlightObject(object); } return; } } // Reveal settings views for (const view of UI.ViewManager.ViewManager.instance().getRegisteredViewExtensions()) { const id = view.viewId(); const location = view.location(); if (location !== UI.ViewManager.ViewLocationValues.SETTINGS_VIEW) { continue; } const settings = view.settings(); if (settings && settings.indexOf(object.name) !== -1) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront(); await SettingsScreen.showSettingsScreen({name: id}); const widget = await view.widget(); if ('highlightObject' in widget && typeof widget.highlightObject === 'function') { widget.highlightObject(object); } return; } } } } export interface ShowSettingsScreenOptions { name?: string; focusTabHeader?: boolean; } export class GreenDevSettingsTab extends UI.Widget.VBox implements SettingsTab { #view: View; constructor(view = GREENDEV_VIEW) { super({jslog: `${VisualLogging.pane('greendev-prototypes')}`}); this.element.id = 'greendev-prototypes-tab-content'; this.#view = view; this.requestUpdate(); } highlightObject(_object: Object): void { } override performUpdate(): Promise<void>|void { const settings = GreenDev.Prototypes.instance().settings(); this.#view({settings}, {}, this.element); } } interface GreenDevViewInput { settings: GreenDev.GreenDevSettings; } type View = (input: GreenDevViewInput, output: object, target: HTMLElement) => void; const GREENDEV_VIEW: View = (input, _output, target) => { // clang-format off render(html` <div class="settings-card-container"> <devtools-card .heading=${'GreenDev prototypes'}> <div class="experiments-warning-subsection"> <devtools-icon .name=${'warning'}></devtools-icon> <span>${i18nString(UIStrings.greenDevUnstable)}</span> </div> <div class="settings-experiments-block"> ${renderPrototypeCheckboxes(input.settings, ['aiAnnotations', 'inDevToolsFloaty'])} </div> </devtools-card> <devtools-card .heading=${'GreenDev widgets'}> <div class="experiments-warning-subsection"> <devtools-icon .name=${'warning'}></devtools-icon> <span>${i18nString(UIStrings.greenDevUnstable)}</span> </div> <div class="settings-experiments-block greendev-widgets"> ${renderWidgetOptions(input.settings)} </div> </devtools-card> </div> `, target); // clang-format on }; const GREENDEV_PROTOTYPE_NAMES: Record<keyof GreenDev.GreenDevSettings, string> = { inDevToolsFloaty: 'In DevTools context picker', aiAnnotations: 'AI auto-annotations', inlineWidgets: 'Inline widgets in AI Assistance', artifactViewer: 'Widgets in the Artifact viewer' }; function renderWidgetOptions(settings: GreenDev.GreenDevSettings): TemplateResult { function onChange(nowActiveRadio: 'inlineWidgets'|'artifactViewer'|'none') { return () => { switch (nowActiveRadio) { case 'inlineWidgets': { settings.artifactViewer.set(false); settings.inlineWidgets.set(true); break; } case 'artifactViewer': { settings.artifactViewer.set(true); settings.inlineWidgets.set(false); break; } case 'none': { settings.artifactViewer.set(false); settings.inlineWidgets.set(false); } } UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning( i18nString(UIStrings.oneOrMoreSettingsHaveChanged)); }; } // clang-format off return html` <p class="settings-experiment"> <label><input type="radio" name="widgets-choice" @change=${onChange('inlineWidgets')}>${GREENDEV_PROTOTYPE_NAMES['inlineWidgets']}</label> </p> <p class="settings-experiment"> <label><input type="radio" name="widgets-choice" @change=${onChange('artifactViewer')}>${GREENDEV_PROTOTYPE_NAMES['artifactViewer']}</label> </p> <p class="settings-experiment"> <label><input type="radio" name="widgets-choice" @change=${onChange('none')}>None</label> </p> `; // clang-format on } function renderPrototypeCheckboxes( settings: GreenDev.GreenDevSettings, keys: Array<keyof GreenDev.GreenDevSettings>, ): TemplateResult { const {bindToSetting} = UI.UIUtils; function showChangeWarning(): void { UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning( i18nString(UIStrings.oneOrMoreSettingsHaveChanged)); } // clang-format off const checkboxes = Object.keys(settings).map(name => { const settingName = name as keyof GreenDev.GreenDevSettings; if(!keys.includes(settingName)) { return nothing; } const setting = settings[settingName]; const title = GREENDEV_PROTOTYPE_NAMES[settingName]; return html`<p class="settings-experiment"> <devtools-checkbox @change=${showChangeWarning} title=${title} ${bindToSetting(setting)}>${title}</devtools-checkbox> </p>`; }); return html`${checkboxes}`; // clang-format on }