blob: 96b36fd9094c2387c1a36b07568aefcf9a2e782c [file] [log] [blame]
Andres Olivares6490c002020-12-02 16:03:351// Copyright 2020 The Chromium Authors. All rights reserved.
Andres Olivares0e3a9e82020-12-01 14:03:202// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Tim van der Lippeaa61faf2021-04-07 15:32:075import * as Common from '../../core/common/common.js';
Jack Franklina75ae7c2021-05-11 13:22:546import type * as Platform from '../../core/platform/platform.js';
Tim van der Lippeaa61faf2021-04-07 15:32:077import * as Root from '../../core/root/root.js';
Andres Olivares0e3a9e82020-12-01 14:03:208
9import {Context} from './Context.js';
10
Andres Olivares0e3a9e82020-12-01 14:03:2011export interface ActionDelegate {
12 handleAction(_context: Context, _actionId: string): boolean;
13}
14
Kateryna Prokopenkoa72448b2021-08-31 14:16:1615export class Action extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
Jan Scheffler01eab3c2021-08-16 17:18:0716 private enabledInternal = true;
17 private toggledInternal = false;
Jack Franklin01d09b02020-12-02 15:15:2018 private actionRegistration: ActionRegistration;
Andres Olivares0e3a9e82020-12-01 14:03:2019 constructor(actionRegistration: ActionRegistration) {
20 super();
Jack Franklin01d09b02020-12-02 15:15:2021 this.actionRegistration = actionRegistration;
Andres Olivares0e3a9e82020-12-01 14:03:2022 }
23
24 id(): string {
Jack Franklin01d09b02020-12-02 15:15:2025 return this.actionRegistration.actionId;
Andres Olivares0e3a9e82020-12-01 14:03:2026 }
27
28 async execute(): Promise<boolean> {
Jack Franklin01d09b02020-12-02 15:15:2029 if (!this.actionRegistration.loadActionDelegate) {
Andres Olivares0e3a9e82020-12-01 14:03:2030 return false;
31 }
Jack Franklin01d09b02020-12-02 15:15:2032 const delegate = await this.actionRegistration.loadActionDelegate();
Andres Olivares0e3a9e82020-12-01 14:03:2033 const actionId = this.id();
34 return delegate.handleAction(Context.instance(), actionId);
35 }
36
37 icon(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:2038 return this.actionRegistration.iconClass;
Andres Olivares0e3a9e82020-12-01 14:03:2039 }
40
41 toggledIcon(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:2042 return this.actionRegistration.toggledIconClass;
Andres Olivares0e3a9e82020-12-01 14:03:2043 }
44
45 toggleWithRedColor(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:3446 return Boolean(this.actionRegistration.toggleWithRedColor);
Andres Olivares0e3a9e82020-12-01 14:03:2047 }
48
Tim van der Lipped946df02020-12-14 14:35:4949 setEnabled(enabled: boolean): void {
Jan Scheffler01eab3c2021-08-16 17:18:0750 if (this.enabledInternal === enabled) {
Andres Olivares0e3a9e82020-12-01 14:03:2051 return;
52 }
53
Jan Scheffler01eab3c2021-08-16 17:18:0754 this.enabledInternal = enabled;
Andres Olivares0e3a9e82020-12-01 14:03:2055 this.dispatchEventToListeners(Events.Enabled, enabled);
56 }
57
58 enabled(): boolean {
Jan Scheffler01eab3c2021-08-16 17:18:0759 return this.enabledInternal;
Andres Olivares0e3a9e82020-12-01 14:03:2060 }
61
62 category(): string {
Jack Franklin01d09b02020-12-02 15:15:2063 return this.actionRegistration.category;
Andres Olivares0e3a9e82020-12-01 14:03:2064 }
65
Andres Olivares344120f2020-12-07 17:43:2866 tags(): string|void {
67 if (this.actionRegistration.tags) {
68 // Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
Andres Olivares4ce36a52021-01-18 18:35:0569 return this.actionRegistration.tags.map(tag => tag()).join('\0');
Andres Olivares344120f2020-12-07 17:43:2870 }
Andres Olivares0e3a9e82020-12-01 14:03:2071 }
72
73 toggleable(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:3474 return Boolean(this.actionRegistration.toggleable);
Andres Olivares0e3a9e82020-12-01 14:03:2075 }
76
77 title(): string {
Andres Olivares4ce36a52021-01-18 18:35:0578 let title = this.actionRegistration.title ? this.actionRegistration.title() : '';
Jack Franklin01d09b02020-12-02 15:15:2079 const options = this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:2080 if (options) {
81 // Actions with an 'options' property don't have a title field. Instead, the displayed
82 // title is taken from the 'title' property of the option that is not active. Only one of the
83 // two options can be active at a given moment and the 'toggled' property of the action along
84 // with the 'value' of the options are used to determine which one it is.
85
86 for (const pair of options) {
Jan Scheffler01eab3c2021-08-16 17:18:0787 if (pair.value !== this.toggledInternal) {
Andres Olivares4ce36a52021-01-18 18:35:0588 title = pair.title();
Andres Olivares0e3a9e82020-12-01 14:03:2089 }
90 }
91 }
92 return title;
93 }
94
95 toggled(): boolean {
Jan Scheffler01eab3c2021-08-16 17:18:0796 return this.toggledInternal;
Andres Olivares0e3a9e82020-12-01 14:03:2097 }
98
Tim van der Lipped946df02020-12-14 14:35:4999 setToggled(toggled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20100 console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id());
Jan Scheffler01eab3c2021-08-16 17:18:07101 if (this.toggledInternal === toggled) {
Andres Olivares0e3a9e82020-12-01 14:03:20102 return;
103 }
104
Jan Scheffler01eab3c2021-08-16 17:18:07105 this.toggledInternal = toggled;
Andres Olivares0e3a9e82020-12-01 14:03:20106 this.dispatchEventToListeners(Events.Toggled, toggled);
107 }
108
109 options(): undefined|Array<ExtensionOption> {
Jack Franklin01d09b02020-12-02 15:15:20110 return this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20111 }
112
Sigurd Schneider61fc9bd2021-07-14 09:01:53113 contextTypes(): undefined|Array<Function> {
Jack Franklin01d09b02020-12-02 15:15:20114 if (this.actionRegistration.contextTypes) {
115 return this.actionRegistration.contextTypes();
Andres Olivares0e3a9e82020-12-01 14:03:20116 }
117 return undefined;
118 }
119
120 canInstantiate(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:34121 return Boolean(this.actionRegistration.loadActionDelegate);
Andres Olivares0e3a9e82020-12-01 14:03:20122 }
123
124 bindings(): Array<Binding>|undefined {
Jack Franklin01d09b02020-12-02 15:15:20125 return this.actionRegistration.bindings;
Andres Olivares0e3a9e82020-12-01 14:03:20126 }
127
128 experiment(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20129 return this.actionRegistration.experiment;
Andres Olivares0e3a9e82020-12-01 14:03:20130 }
131
132 condition(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20133 return this.actionRegistration.condition;
Andres Olivares0e3a9e82020-12-01 14:03:20134 }
Andres Olivares4dba1302021-01-28 23:17:32135
136 order(): number|undefined {
137 return this.actionRegistration.order;
138 }
Andres Olivares0e3a9e82020-12-01 14:03:20139}
140
Andres Olivaresf2a2ddd2021-02-03 17:27:51141const registeredActionExtensions: Array<Action> = [];
Andres Olivares0e3a9e82020-12-01 14:03:20142
143const actionIdSet = new Set<string>();
144
Tim van der Lipped946df02020-12-14 14:35:49145export function registerActionExtension(registration: ActionRegistration): void {
Andres Olivares0e3a9e82020-12-01 14:03:20146 const actionId = registration.actionId;
147 if (actionIdSet.has(actionId)) {
148 throw new Error(`Duplicate Action id '${actionId}': ${new Error().stack}`);
149 }
150 actionIdSet.add(actionId);
Andres Olivaresf2a2ddd2021-02-03 17:27:51151 registeredActionExtensions.push(new Action(registration));
Andres Olivares0e3a9e82020-12-01 14:03:20152}
153
Andrés Olivaresb0fcd182023-02-21 10:13:28154export function reset(): void {
155 actionIdSet.clear();
156 registeredActionExtensions.length = 0;
157}
158
Andres Olivaresf2a2ddd2021-02-03 17:27:51159export function getRegisteredActionExtensions(): Array<Action> {
Andres Olivares4dba1302021-01-28 23:17:32160 return registeredActionExtensions
161 .filter(
162 action => Root.Runtime.Runtime.isDescriptorEnabled(
163 {experiment: action.experiment(), condition: action.condition()}))
164 .sort((firstAction, secondAction) => {
165 const order1 = firstAction.order() || 0;
166 const order2 = secondAction.order() || 0;
167 return order1 - order2;
168 });
Andres Olivares0e3a9e82020-12-01 14:03:20169}
170
Andres Olivares975c3022021-03-29 13:45:24171export function maybeRemoveActionExtension(actionId: string): boolean {
172 const actionIndex = registeredActionExtensions.findIndex(action => action.id() === actionId);
173 if (actionIndex < 0 || !actionIdSet.delete(actionId)) {
174 return false;
175 }
176 registeredActionExtensions.splice(actionIndex, 1);
177 return true;
178}
179
Andres Olivares4ce36a52021-01-18 18:35:05180export const enum Platforms {
Andres Olivares0e3a9e82020-12-01 14:03:20181 All = 'All platforms',
182 Mac = 'mac',
183 WindowsLinux = 'windows,linux',
184 Android = 'Android',
Andres Olivares70556d62021-02-02 19:09:01185 Windows = 'windows',
Andres Olivares0e3a9e82020-12-01 14:03:20186}
187
Kateryna Prokopenkoa72448b2021-08-31 14:16:16188export const enum Events {
189 Enabled = 'Enabled',
190 Toggled = 'Toggled',
191}
192
193export type EventTypes = {
194 [Events.Enabled]: boolean,
195 [Events.Toggled]: boolean,
Andres Olivares0e3a9e82020-12-01 14:03:20196};
197
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04198// TODO(crbug.com/1181019)
Andres Olivares0e3a9e82020-12-01 14:03:20199export const ActionCategory = {
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04200 ELEMENTS: 'Elements',
201 SCREENSHOT: 'Screenshot',
202 NETWORK: 'Network',
203 MEMORY: 'Memory',
204 JAVASCRIPT_PROFILER: 'JavaScript Profiler',
205 CONSOLE: 'Console',
206 PERFORMANCE: 'Performance',
207 MOBILE: 'Mobile',
208 SENSORS: 'Sensors',
209 HELP: 'Help',
210 INPUTS: 'Inputs',
211 LAYERS: 'Layers',
212 NAVIGATION: 'Navigation',
213 DRAWER: 'Drawer',
214 GLOBAL: 'Global',
215 RESOURCES: 'Resources',
216 BACKGROUND_SERVICES: 'Background Services',
217 SETTINGS: 'Settings',
218 DEBUGGER: 'Debugger',
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04219 SOURCES: 'Sources',
Jacky Hu4853b342022-04-29 10:47:21220 RENDERING: 'Rendering',
Andres Olivares0e3a9e82020-12-01 14:03:20221};
222
223type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
224
225export const enum IconClass {
Kateryna Prokopenko910cb532023-03-28 09:36:46226 LARGEICON_NODE_SEARCH = 'select-element',
Kateryna Prokopenko75da9712023-03-31 11:56:38227 START_RECORDING = 'record-start',
228 STOP_RECORDING = 'record-stop',
Kateryna Prokopenko551f9c82023-04-04 14:34:21229 REFRESH = 'refresh',
Kateryna Prokopenko05ade9a2023-03-31 10:57:05230 CLEAR = 'clear',
231 EYE = 'eye',
Kateryna Prokopenko910cb532023-03-28 09:36:46232 LARGEICON_PHONE = 'devices',
Andres Olivaresa7b7b492021-01-28 16:12:37233 LARGEICON_PLAY = 'largeicon-play',
Alex Rudenkoa2ffe4a2021-05-17 13:36:31234 LARGEICON_DOWNLOAD = 'largeicon-download',
Benedikt Meurer4878f6a2023-03-29 12:01:44235 LARGEICON_PAUSE = 'pause',
236 LARGEICON_RESUME = 'resume',
Benedikt Meurer178c0c32023-03-31 04:50:06237 BIN = 'bin',
Kateryna Prokopenko910cb532023-03-28 09:36:46238 LARGEICON_SETTINGS_GEAR = 'gear',
Benedikt Meurer4878f6a2023-03-29 12:01:44239 LARGEICON_STEP_OVER = 'step-over',
240 LARGE_ICON_STEP_INTO = 'step-into',
241 LARGE_ICON_STEP = 'step',
242 LARGE_ICON_STEP_OUT = 'step-out',
Benedikt Meurer32c2a1b2023-03-30 07:09:59243 BREAKPOINT_CROSSED_FILLED = 'breakpoint-crossed-filled',
244 BREAKPOINT_CROSSED = 'breakpoint-crossed',
Andres Olivares70556d62021-02-02 19:09:01245 LARGE_ICON_ADD = 'largeicon-add',
Andres Olivares0e3a9e82020-12-01 14:03:20246}
247
248export const enum KeybindSet {
249 DEVTOOLS_DEFAULT = 'devToolsDefault',
250 VS_CODE = 'vsCode',
251}
252
253export interface ExtensionOption {
254 value: boolean;
Andres Olivares4ce36a52021-01-18 18:35:05255 title: () => Platform.UIString.LocalizedString;
Andres Olivares0e3a9e82020-12-01 14:03:20256 text?: string;
257}
258
259export interface Binding {
Andres Olivares4ce36a52021-01-18 18:35:05260 platform?: Platforms;
Andres Olivares0e3a9e82020-12-01 14:03:20261 shortcut: string;
262 keybindSets?: Array<KeybindSet>;
263}
264
Andres Olivares3f49f132020-12-03 13:10:27265/**
266 * The representation of an action extension to be registered.
267 */
Andres Olivares0e3a9e82020-12-01 14:03:20268export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27269 /**
270 * The unique id of an Action extension.
271 */
Andres Olivares0e3a9e82020-12-01 14:03:20272 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27273 /**
274 * The category with which the action is displayed in the UI.
275 */
Andres Olivares0e3a9e82020-12-01 14:03:20276 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27277 /**
278 * The title with which the action is displayed in the UI.
279 */
Andres Olivares4ce36a52021-01-18 18:35:05280 title?: () => Platform.UIString.LocalizedString;
Andres Olivares3f49f132020-12-03 13:10:27281 /**
282 * The type of the icon used to trigger the action.
283 */
Andres Olivares844c58d2021-01-26 13:09:07284 iconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27285 /**
286 * Whether the style of the icon toggles on interaction.
287 */
Andres Olivares844c58d2021-01-26 13:09:07288 toggledIconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27289 /**
290 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
291 */
Andres Olivares0e3a9e82020-12-01 14:03:20292 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27293 /**
294 * Words used to find an action in the Command Menu.
295 */
Andres Olivares4ce36a52021-01-18 18:35:05296 tags?: Array<() => Platform.UIString.LocalizedString>;
Andres Olivares3f49f132020-12-03 13:10:27297 /**
298 * Whether the action is toggleable.
299 */
Andres Olivares0e3a9e82020-12-01 14:03:20300 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27301 /**
Andres Olivaresa0cdb382021-01-21 15:44:33302 * Loads the class that handles the action when it is triggered. The common pattern for implementing
303 * this function relies on having the module that contains the action’s handler lazily loaded. For example:
304 * ```js
305 * let loadedElementsModule;
306 *
307 * async function loadElementsModule() {
308 *
309 * if (!loadedElementsModule) {
310 * loadedElementsModule = await import('./elements.js');
311 * }
312 * return loadedElementsModule;
313 * }
314 * UI.ActionRegistration.registerActionExtension({
315 * <...>
316 * async loadActionDelegate() {
317 * const Elements = await loadElementsModule();
318 * return Elements.ElementsPanel.ElementsActionDelegate.instance();
319 * },
320 * <...>
321 * });
322 * ```
Andres Olivares3f49f132020-12-03 13:10:27323 */
Andres Olivares0e3a9e82020-12-01 14:03:20324 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27325 /**
326 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
327 * The context of the application is described in 'flavors' that are usually views added and removed to the context
328 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
329 * When the action is supposed to be available globally, that is, it does not depend on the application to have
330 * a specific context, the value of this property should be undefined.
331 *
332 * Because the method is synchronous, context types should be already loaded when the method is invoked.
333 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
334 * return an empty array. Once the context types have been loaded, the function should return an array with all types
335 * that it depends on.
336 *
337 * The common pattern for implementing this function is relying on having the module with the corresponding context
338 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
339 *
340 * ```js
341 * let loadedElementsModule;
342 *
343 * async function loadElementsModule() {
344 *
345 * if (!loadedElementsModule) {
346 * loadedElementsModule = await import('./elements.js');
347 * }
348 * return loadedElementsModule;
349 * }
350 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
351 *
352 * if (loadedElementsModule === undefined) {
353 * return [];
354 * }
355 * return getClassCallBack(loadedElementsModule);
356 * }
357 * UI.ActionRegistration.registerActionExtension({
358 *
359 * contextTypes() {
360 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
361 * }
362 * <...>
363 * });
364 * ```
365 */
Sigurd Schneider61fc9bd2021-07-14 09:01:53366 contextTypes?: () => Array<Function>;
Andres Olivares3f49f132020-12-03 13:10:27367 /**
Andres Olivaresa0cdb382021-01-21 15:44:33368 * The descriptions for each of the two states in which a toggleable action can be.
Andres Olivares3f49f132020-12-03 13:10:27369 */
Andres Olivares0e3a9e82020-12-01 14:03:20370 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27371 /**
372 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
373 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
374 * and the keybindSet property.
375 *
376 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
377 * are flavors of the current appliaction context.
378 */
Andres Olivares0e3a9e82020-12-01 14:03:20379 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27380 /**
Andres Olivaresa0cdb382021-01-21 15:44:33381 * The name of the experiment an action is associated with. Enabling and disabling the declared
382 * experiment will enable and disable the action respectively.
Andres Olivares3f49f132020-12-03 13:10:27383 */
Andres Olivares0e3a9e82020-12-01 14:03:20384 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27385 /**
Andres Olivaresa0cdb382021-01-21 15:44:33386 * A condition represented as a string the action's availability depends on. Conditions come
387 * from the queryParamsObject defined in Runtime and just as the experiment field, they determine the availability
388 * of the setting. A condition can be negated by prepending a ‘!’ to the value of the condition
389 * property and in that case the behaviour of the action's availability will be inverted.
Andres Olivares3f49f132020-12-03 13:10:27390 */
Andres Olivares0e3a9e82020-12-01 14:03:20391 condition?: Root.Runtime.ConditionName;
Andres Olivares4dba1302021-01-28 23:17:32392 /**
393 * Used to sort actions when all registered actions are queried.
394 */
395 order?: number;
Andres Olivares0e3a9e82020-12-01 14:03:20396}