| // Copyright 2020 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 * as Common from '../../core/common/common.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as Root from '../../core/root/root.js'; |
| |
| import {Context} from './Context.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Title of the keybind category 'Elements' in Settings' Shortcuts pannel. |
| */ |
| elements: 'Elements', |
| /** |
| *@description Title of the keybind category 'Screenshot' in Settings' Shortcuts pannel. |
| */ |
| screenshot: 'Screenshot', |
| /** |
| *@description Title of the keybind category 'Network' in Settings' Shortcuts pannel. |
| */ |
| network: 'Network', |
| /** |
| *@description Title of the keybind category 'Memory' in Settings' Shortcuts pannel. |
| */ |
| memory: 'Memory', |
| /** |
| *@description Title of the keybind category 'JavaScript Profiler' in Settings' Shortcuts pannel. |
| */ |
| javascript_profiler: 'JavaScript Profiler', |
| /** |
| *@description Title of the keybind category 'Console' in Settings' Shortcuts pannel. |
| */ |
| console: 'Console', |
| /** |
| *@description Title of the keybind category 'Performance' in Settings' Shortcuts pannel. |
| */ |
| performance: 'Performance', |
| /** |
| *@description Title of the keybind category 'Mobile' in Settings' Shortcuts pannel. |
| */ |
| mobile: 'Mobile', |
| /** |
| *@description Title of the keybind category 'Help' in Settings' Shortcuts pannel. |
| */ |
| help: 'Help', |
| /** |
| *@description Title of the keybind category 'Layers' in Settings' Shortcuts pannel. |
| */ |
| layers: 'Layers', |
| /** |
| *@description Title of the keybind category 'Navigation' in Settings' Shortcuts pannel. |
| */ |
| navigation: 'Navigation', |
| /** |
| *@description Title of the keybind category 'Drawer' in Settings' Shortcuts pannel. |
| */ |
| drawer: 'Drawer', |
| /** |
| *@description Title of the keybind category 'Global' in Settings' Shortcuts pannel. |
| */ |
| global: 'Global', |
| /** |
| *@description Title of the keybind category 'Resources' in Settings' Shortcuts pannel. |
| */ |
| resources: 'Resources', |
| /** |
| *@description Title of the keybind category 'Background Services' in Settings' Shortcuts pannel. |
| */ |
| background_services: 'Background Services', |
| /** |
| *@description Title of the keybind category 'Settings' in Settings' Shortcuts pannel. |
| */ |
| settings: 'Settings', |
| /** |
| *@description Title of the keybind category 'Debugger' in Settings' Shortcuts pannel. |
| */ |
| debugger: 'Debugger', |
| /** |
| *@description Title of the keybind category 'Sources' in Settings' Shortcuts pannel. |
| */ |
| sources: 'Sources', |
| /** |
| *@description Title of the keybind category 'Rendering' in Settings' Shortcuts pannel. |
| */ |
| rendering: 'Rendering', |
| /** |
| *@description Title of the keybind category 'Recorder' in Settings' Shortcuts pannel. |
| */ |
| recorder: 'Recorder', |
| /** |
| *@description Title of the keybind category 'Changes' in Settings' Shortcuts pannel. |
| */ |
| changes: 'Changes', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('ui/legacy/ActionRegistration.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export interface ActionDelegate { |
| handleAction(context: Context, actionId: string): boolean; |
| } |
| |
| export class Action extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { |
| private enabledInternal = true; |
| private toggledInternal = false; |
| private actionRegistration: ActionRegistration; |
| constructor(actionRegistration: ActionRegistration) { |
| super(); |
| this.actionRegistration = actionRegistration; |
| } |
| |
| id(): string { |
| return this.actionRegistration.actionId; |
| } |
| |
| async execute(): Promise<boolean> { |
| if (!this.actionRegistration.loadActionDelegate) { |
| return false; |
| } |
| const delegate = await this.actionRegistration.loadActionDelegate(); |
| const actionId = this.id(); |
| return delegate.handleAction(Context.instance(), actionId); |
| } |
| |
| icon(): string|undefined { |
| return this.actionRegistration.iconClass; |
| } |
| |
| toggledIcon(): string|undefined { |
| return this.actionRegistration.toggledIconClass; |
| } |
| |
| toggleWithRedColor(): boolean { |
| return Boolean(this.actionRegistration.toggleWithRedColor); |
| } |
| |
| setEnabled(enabled: boolean): void { |
| if (this.enabledInternal === enabled) { |
| return; |
| } |
| |
| this.enabledInternal = enabled; |
| this.dispatchEventToListeners(Events.ENABLED, enabled); |
| } |
| |
| enabled(): boolean { |
| return this.enabledInternal; |
| } |
| |
| category(): ActionCategory { |
| return this.actionRegistration.category; |
| } |
| |
| tags(): string|void { |
| if (this.actionRegistration.tags) { |
| // Get localized keys and separate by null character to prevent fuzzy matching from matching across them. |
| return this.actionRegistration.tags.map(tag => tag()).join('\0'); |
| } |
| } |
| |
| toggleable(): boolean { |
| return Boolean(this.actionRegistration.toggleable); |
| } |
| |
| title(): Common.UIString.LocalizedString { |
| let title = this.actionRegistration.title ? this.actionRegistration.title() : i18n.i18n.lockedString(''); |
| const options = this.actionRegistration.options; |
| if (options) { |
| // Actions with an 'options' property don't have a title field. Instead, the displayed |
| // title is taken from the 'title' property of the option that is not active. Only one of the |
| // two options can be active at a given moment and the 'toggled' property of the action along |
| // with the 'value' of the options are used to determine which one it is. |
| |
| for (const pair of options) { |
| if (pair.value !== this.toggledInternal) { |
| title = pair.title(); |
| } |
| } |
| } |
| return title; |
| } |
| |
| toggled(): boolean { |
| return this.toggledInternal; |
| } |
| |
| setToggled(toggled: boolean): void { |
| console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id()); |
| if (this.toggledInternal === toggled) { |
| return; |
| } |
| |
| this.toggledInternal = toggled; |
| this.dispatchEventToListeners(Events.TOGGLED, toggled); |
| } |
| |
| options(): undefined|ExtensionOption[] { |
| return this.actionRegistration.options; |
| } |
| |
| contextTypes(): undefined|Array<Platform.Constructor.Constructor<unknown>> { |
| if (this.actionRegistration.contextTypes) { |
| return this.actionRegistration.contextTypes(); |
| } |
| return undefined; |
| } |
| |
| canInstantiate(): boolean { |
| return Boolean(this.actionRegistration.loadActionDelegate); |
| } |
| |
| bindings(): Binding[]|undefined { |
| return this.actionRegistration.bindings; |
| } |
| |
| experiment(): string|undefined { |
| return this.actionRegistration.experiment; |
| } |
| |
| setting(): string|undefined { |
| return this.actionRegistration.setting; |
| } |
| |
| condition(): Root.Runtime.Condition|undefined { |
| return this.actionRegistration.condition; |
| } |
| |
| order(): number|undefined { |
| return this.actionRegistration.order; |
| } |
| } |
| |
| const registeredActions = new Map<string, Action>(); |
| |
| export function registerActionExtension(registration: ActionRegistration): void { |
| const actionId = registration.actionId; |
| if (registeredActions.has(actionId)) { |
| throw new Error(`Duplicate action ID '${actionId}'`); |
| } |
| if (!Platform.StringUtilities.isExtendedKebabCase(actionId)) { |
| throw new Error(`Invalid action ID '${actionId}'`); |
| } |
| registeredActions.set(actionId, new Action(registration)); |
| } |
| |
| export function reset(): void { |
| registeredActions.clear(); |
| } |
| |
| export function getRegisteredActionExtensions(): Action[] { |
| return Array.from(registeredActions.values()) |
| .filter(action => { |
| const settingName = action.setting(); |
| try { |
| if (settingName && !Common.Settings.moduleSetting(settingName).get()) { |
| return false; |
| } |
| } catch (err) { |
| if (err.message.startsWith('No setting registered')) { |
| return false; |
| } |
| } |
| |
| return Root.Runtime.Runtime.isDescriptorEnabled({ |
| experiment: action.experiment(), |
| condition: action.condition(), |
| }); |
| }) |
| .sort((firstAction, secondAction) => { |
| const order1 = firstAction.order() || 0; |
| const order2 = secondAction.order() || 0; |
| return order1 - order2; |
| }); |
| } |
| |
| export function maybeRemoveActionExtension(actionId: string): boolean { |
| return registeredActions.delete(actionId); |
| } |
| |
| export const enum Platforms { |
| ALL = 'All platforms', |
| MAC = 'mac', |
| WINDOWS_LINUX = 'windows,linux', |
| ANDROID = 'Android', |
| WINDOWS = 'windows', |
| } |
| |
| export const enum Events { |
| ENABLED = 'Enabled', |
| TOGGLED = 'Toggled', |
| } |
| |
| export interface EventTypes { |
| [Events.ENABLED]: boolean; |
| [Events.TOGGLED]: boolean; |
| } |
| |
| export const enum ActionCategory { |
| NONE = '', // `NONE` must be a falsy value. Legacy code uses if-checks for the category. |
| ELEMENTS = 'ELEMENTS', |
| SCREENSHOT = 'SCREENSHOT', |
| NETWORK = 'NETWORK', |
| MEMORY = 'MEMORY', |
| JAVASCRIPT_PROFILER = 'JAVASCRIPT_PROFILER', |
| CONSOLE = 'CONSOLE', |
| PERFORMANCE = 'PERFORMANCE', |
| MOBILE = 'MOBILE', |
| HELP = 'HELP', |
| LAYERS = 'LAYERS', |
| NAVIGATION = 'NAVIGATION', |
| DRAWER = 'DRAWER', |
| GLOBAL = 'GLOBAL', |
| RESOURCES = 'RESOURCES', |
| BACKGROUND_SERVICES = 'BACKGROUND_SERVICES', |
| SETTINGS = 'SETTINGS', |
| DEBUGGER = 'DEBUGGER', |
| SOURCES = 'SOURCES', |
| RENDERING = 'RENDERING', |
| RECORDER = 'RECORDER', |
| CHANGES = 'CHANGES', |
| } |
| |
| export function getLocalizedActionCategory(category: ActionCategory): Platform.UIString.LocalizedString { |
| switch (category) { |
| case ActionCategory.ELEMENTS: |
| return i18nString(UIStrings.elements); |
| case ActionCategory.SCREENSHOT: |
| return i18nString(UIStrings.screenshot); |
| case ActionCategory.NETWORK: |
| return i18nString(UIStrings.network); |
| case ActionCategory.MEMORY: |
| return i18nString(UIStrings.memory); |
| case ActionCategory.JAVASCRIPT_PROFILER: |
| return i18nString(UIStrings.javascript_profiler); |
| case ActionCategory.CONSOLE: |
| return i18nString(UIStrings.console); |
| case ActionCategory.PERFORMANCE: |
| return i18nString(UIStrings.performance); |
| case ActionCategory.MOBILE: |
| return i18nString(UIStrings.mobile); |
| case ActionCategory.HELP: |
| return i18nString(UIStrings.help); |
| case ActionCategory.LAYERS: |
| return i18nString(UIStrings.layers); |
| case ActionCategory.NAVIGATION: |
| return i18nString(UIStrings.navigation); |
| case ActionCategory.DRAWER: |
| return i18nString(UIStrings.drawer); |
| case ActionCategory.GLOBAL: |
| return i18nString(UIStrings.global); |
| case ActionCategory.RESOURCES: |
| return i18nString(UIStrings.resources); |
| case ActionCategory.BACKGROUND_SERVICES: |
| return i18nString(UIStrings.background_services); |
| case ActionCategory.SETTINGS: |
| return i18nString(UIStrings.settings); |
| case ActionCategory.DEBUGGER: |
| return i18nString(UIStrings.debugger); |
| case ActionCategory.SOURCES: |
| return i18nString(UIStrings.sources); |
| case ActionCategory.RENDERING: |
| return i18nString(UIStrings.rendering); |
| case ActionCategory.RECORDER: |
| return i18nString(UIStrings.recorder); |
| case ActionCategory.CHANGES: |
| return i18nString(UIStrings.changes); |
| case ActionCategory.NONE: |
| return i18n.i18n.lockedString(''); |
| } |
| // Not all categories are cleanly typed yet. Return the category as-is in this case. |
| return i18n.i18n.lockedString(category); |
| } |
| |
| export const enum IconClass { |
| LARGEICON_NODE_SEARCH = 'select-element', |
| START_RECORDING = 'record-start', |
| STOP_RECORDING = 'record-stop', |
| REFRESH = 'refresh', |
| CLEAR = 'clear', |
| EYE = 'eye', |
| LARGEICON_PHONE = 'devices', |
| PLAY = 'play', |
| DOWNLOAD = 'download', |
| LARGEICON_PAUSE = 'pause', |
| LARGEICON_RESUME = 'resume', |
| MOP = 'mop', |
| BIN = 'bin', |
| LARGEICON_SETTINGS_GEAR = 'gear', |
| LARGEICON_STEP_OVER = 'step-over', |
| LARGE_ICON_STEP_INTO = 'step-into', |
| LARGE_ICON_STEP = 'step', |
| LARGE_ICON_STEP_OUT = 'step-out', |
| BREAKPOINT_CROSSED_FILLED = 'breakpoint-crossed-filled', |
| BREAKPOINT_CROSSED = 'breakpoint-crossed', |
| PLUS = 'plus', |
| UNDO = 'undo', |
| COPY = 'copy', |
| IMPORT = 'import', |
| } |
| |
| export const enum KeybindSet { |
| DEVTOOLS_DEFAULT = 'devToolsDefault', |
| VS_CODE = 'vsCode', |
| } |
| |
| export interface ExtensionOption { |
| value: boolean; |
| title: () => Platform.UIString.LocalizedString; |
| text?: string; |
| } |
| |
| export interface Binding { |
| platform?: Platforms; |
| shortcut: string; |
| keybindSets?: KeybindSet[]; |
| } |
| |
| /** |
| * The representation of an action extension to be registered. |
| */ |
| export interface ActionRegistration { |
| /** |
| * The unique id of an Action extension. |
| */ |
| actionId: string; |
| /** |
| * The category with which the action is displayed in the UI. |
| */ |
| category: ActionCategory; |
| /** |
| * The title with which the action is displayed in the UI. |
| */ |
| title?: () => Platform.UIString.LocalizedString; |
| /** |
| * The type of the icon used to trigger the action. |
| */ |
| iconClass?: IconClass; |
| /** |
| * Whether the style of the icon toggles on interaction. |
| */ |
| toggledIconClass?: IconClass; |
| /** |
| * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction. |
| */ |
| toggleWithRedColor?: boolean; |
| /** |
| * Words used to find an action in the Command Menu. |
| */ |
| tags?: Array<() => Platform.UIString.LocalizedString>; |
| /** |
| * Whether the action is toggleable. |
| */ |
| toggleable?: boolean; |
| /** |
| * Loads the class that handles the action when it is triggered. The common pattern for implementing |
| * this function relies on having the module that contains the action’s handler lazily loaded. For example: |
| * ```js |
| * let loadedElementsModule; |
| * |
| * async function loadElementsModule() { |
| * |
| * if (!loadedElementsModule) { |
| * loadedElementsModule = await import('./elements.js'); |
| * } |
| * return loadedElementsModule; |
| * } |
| * UI.ActionRegistration.registerActionExtension({ |
| * <...> |
| * async loadActionDelegate() { |
| * const Elements = await loadElementsModule(); |
| * return new Elements.ElementsPanel.ElementsActionDelegate(); |
| * }, |
| * <...> |
| * }); |
| * ``` |
| */ |
| loadActionDelegate?: () => Promise<ActionDelegate>; |
| /** |
| * Returns the classes that represent the 'context flavors' under which the action is available for triggering. |
| * The context of the application is described in 'flavors' that are usually views added and removed to the context |
| * as the user interacts with the application (e.g when the user moves across views). (See UI.Context) |
| * When the action is supposed to be available globally, that is, it does not depend on the application to have |
| * a specific context, the value of this property should be undefined. |
| * |
| * Because the method is synchronous, context types should be already loaded when the method is invoked. |
| * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should |
| * return an empty array. Once the context types have been loaded, the function should return an array with all types |
| * that it depends on. |
| * |
| * The common pattern for implementing this function is relying on having the module with the corresponding context |
| * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example: |
| * |
| * ```js |
| * let loadedElementsModule; |
| * |
| * async function loadElementsModule() { |
| * |
| * if (!loadedElementsModule) { |
| * loadedElementsModule = await import('./elements.js'); |
| * } |
| * return loadedElementsModule; |
| * } |
| * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] { |
| * |
| * if (loadedElementsModule === undefined) { |
| * return []; |
| * } |
| * return getClassCallBack(loadedElementsModule); |
| * } |
| * UI.ActionRegistration.registerActionExtension({ |
| * |
| * contextTypes() { |
| * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]); |
| * } |
| * <...> |
| * }); |
| * ``` |
| */ |
| contextTypes?: () => Array<Platform.Constructor.Constructor<unknown>>; |
| /** |
| * The descriptions for each of the two states in which a toggleable action can be. |
| */ |
| options?: ExtensionOption[]; |
| /** |
| * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action. |
| * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets |
| * and the keybindSet property. |
| * |
| * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types |
| * are flavors of the current appliaction context. |
| */ |
| bindings?: Binding[]; |
| /** |
| * The name of the experiment an action is associated with. Enabling and disabling the declared |
| * experiment will enable and disable the action respectively. |
| */ |
| experiment?: Root.Runtime.ExperimentName; |
| /** |
| * The name of the setting an action is associated with. Enabling and |
| * disabling the declared setting will enable and disable the action |
| * respectively. Note that changing the setting requires a reload for it to |
| * apply to action registration. |
| */ |
| setting?: string; |
| /** |
| * A condition is a function that will make the action available if it |
| * returns true, and not available, otherwise. Make sure that objects you |
| * access from inside the condition function are ready at the time when the |
| * setting conditions are checked. |
| */ |
| condition?: Root.Runtime.Condition; |
| /** |
| * Used to sort actions when all registered actions are queried. |
| */ |
| order?: number; |
| } |