blob: 763b15b840cd229336f3bd994a499fa06fe4b577 [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',
Andres Olivaresd7b94d62020-12-14 21:01:38227 LARGEICON_START_RECORDING = 'largeicon-start-recording',
228 LARGEICON_STOP_RECORDING = 'largeicon-stop-recording',
Andres Olivares37a21772020-12-22 14:10:09229 LARGEICON_REFRESH = 'largeicon-refresh',
Andres Olivares844c58d2021-01-26 13:09:07230 LARGEICON_CLEAR = 'largeicon-clear',
231 LARGEICON_VISIBILITY = 'largeicon-visibility',
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',
Andres Olivaresa7b7b492021-01-28 16:12:37235 LARGEICON_PAUSE = 'largeicon-pause',
236 LARGEICON_RESUME = 'largeicon-resume',
Andres Olivares27dccc02021-02-01 13:31:16237 LARGEICON_TRASH_BIN = 'largeicon-trash-bin',
Kateryna Prokopenko910cb532023-03-28 09:36:46238 LARGEICON_SETTINGS_GEAR = 'gear',
Andres Olivares7ed98072021-02-02 18:13:58239 LARGEICON_STEP_OVER = 'largeicon-step-over',
240 LARGE_ICON_STEP_INTO = 'largeicon-step-into',
241 LARGE_ICON_STEP = 'largeicon-step',
242 LARGE_ICON_STEP_OUT = 'largeicon-step-out',
243 LARGE_ICON_DEACTIVATE_BREAKPOINTS = 'largeicon-deactivate-breakpoints',
Andres Olivares70556d62021-02-02 19:09:01244 LARGE_ICON_ADD = 'largeicon-add',
Andres Olivares0e3a9e82020-12-01 14:03:20245}
246
247export const enum KeybindSet {
248 DEVTOOLS_DEFAULT = 'devToolsDefault',
249 VS_CODE = 'vsCode',
250}
251
252export interface ExtensionOption {
253 value: boolean;
Andres Olivares4ce36a52021-01-18 18:35:05254 title: () => Platform.UIString.LocalizedString;
Andres Olivares0e3a9e82020-12-01 14:03:20255 text?: string;
256}
257
258export interface Binding {
Andres Olivares4ce36a52021-01-18 18:35:05259 platform?: Platforms;
Andres Olivares0e3a9e82020-12-01 14:03:20260 shortcut: string;
261 keybindSets?: Array<KeybindSet>;
262}
263
Andres Olivares3f49f132020-12-03 13:10:27264/**
265 * The representation of an action extension to be registered.
266 */
Andres Olivares0e3a9e82020-12-01 14:03:20267export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27268 /**
269 * The unique id of an Action extension.
270 */
Andres Olivares0e3a9e82020-12-01 14:03:20271 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27272 /**
273 * The category with which the action is displayed in the UI.
274 */
Andres Olivares0e3a9e82020-12-01 14:03:20275 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27276 /**
277 * The title with which the action is displayed in the UI.
278 */
Andres Olivares4ce36a52021-01-18 18:35:05279 title?: () => Platform.UIString.LocalizedString;
Andres Olivares3f49f132020-12-03 13:10:27280 /**
281 * The type of the icon used to trigger the action.
282 */
Andres Olivares844c58d2021-01-26 13:09:07283 iconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27284 /**
285 * Whether the style of the icon toggles on interaction.
286 */
Andres Olivares844c58d2021-01-26 13:09:07287 toggledIconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27288 /**
289 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
290 */
Andres Olivares0e3a9e82020-12-01 14:03:20291 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27292 /**
293 * Words used to find an action in the Command Menu.
294 */
Andres Olivares4ce36a52021-01-18 18:35:05295 tags?: Array<() => Platform.UIString.LocalizedString>;
Andres Olivares3f49f132020-12-03 13:10:27296 /**
297 * Whether the action is toggleable.
298 */
Andres Olivares0e3a9e82020-12-01 14:03:20299 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27300 /**
Andres Olivaresa0cdb382021-01-21 15:44:33301 * Loads the class that handles the action when it is triggered. The common pattern for implementing
302 * this function relies on having the module that contains the action’s handler lazily loaded. For example:
303 * ```js
304 * let loadedElementsModule;
305 *
306 * async function loadElementsModule() {
307 *
308 * if (!loadedElementsModule) {
309 * loadedElementsModule = await import('./elements.js');
310 * }
311 * return loadedElementsModule;
312 * }
313 * UI.ActionRegistration.registerActionExtension({
314 * <...>
315 * async loadActionDelegate() {
316 * const Elements = await loadElementsModule();
317 * return Elements.ElementsPanel.ElementsActionDelegate.instance();
318 * },
319 * <...>
320 * });
321 * ```
Andres Olivares3f49f132020-12-03 13:10:27322 */
Andres Olivares0e3a9e82020-12-01 14:03:20323 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27324 /**
325 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
326 * The context of the application is described in 'flavors' that are usually views added and removed to the context
327 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
328 * When the action is supposed to be available globally, that is, it does not depend on the application to have
329 * a specific context, the value of this property should be undefined.
330 *
331 * Because the method is synchronous, context types should be already loaded when the method is invoked.
332 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
333 * return an empty array. Once the context types have been loaded, the function should return an array with all types
334 * that it depends on.
335 *
336 * The common pattern for implementing this function is relying on having the module with the corresponding context
337 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
338 *
339 * ```js
340 * let loadedElementsModule;
341 *
342 * async function loadElementsModule() {
343 *
344 * if (!loadedElementsModule) {
345 * loadedElementsModule = await import('./elements.js');
346 * }
347 * return loadedElementsModule;
348 * }
349 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
350 *
351 * if (loadedElementsModule === undefined) {
352 * return [];
353 * }
354 * return getClassCallBack(loadedElementsModule);
355 * }
356 * UI.ActionRegistration.registerActionExtension({
357 *
358 * contextTypes() {
359 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
360 * }
361 * <...>
362 * });
363 * ```
364 */
Sigurd Schneider61fc9bd2021-07-14 09:01:53365 contextTypes?: () => Array<Function>;
Andres Olivares3f49f132020-12-03 13:10:27366 /**
Andres Olivaresa0cdb382021-01-21 15:44:33367 * The descriptions for each of the two states in which a toggleable action can be.
Andres Olivares3f49f132020-12-03 13:10:27368 */
Andres Olivares0e3a9e82020-12-01 14:03:20369 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27370 /**
371 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
372 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
373 * and the keybindSet property.
374 *
375 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
376 * are flavors of the current appliaction context.
377 */
Andres Olivares0e3a9e82020-12-01 14:03:20378 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27379 /**
Andres Olivaresa0cdb382021-01-21 15:44:33380 * The name of the experiment an action is associated with. Enabling and disabling the declared
381 * experiment will enable and disable the action respectively.
Andres Olivares3f49f132020-12-03 13:10:27382 */
Andres Olivares0e3a9e82020-12-01 14:03:20383 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27384 /**
Andres Olivaresa0cdb382021-01-21 15:44:33385 * A condition represented as a string the action's availability depends on. Conditions come
386 * from the queryParamsObject defined in Runtime and just as the experiment field, they determine the availability
387 * of the setting. A condition can be negated by prepending a ‘!’ to the value of the condition
388 * property and in that case the behaviour of the action's availability will be inverted.
Andres Olivares3f49f132020-12-03 13:10:27389 */
Andres Olivares0e3a9e82020-12-01 14:03:20390 condition?: Root.Runtime.ConditionName;
Andres Olivares4dba1302021-01-28 23:17:32391 /**
392 * Used to sort actions when all registered actions are queried.
393 */
394 order?: number;
Andres Olivares0e3a9e82020-12-01 14:03:20395}