blob: da133b431aca81041d3fe85200e196da63c215e5 [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
Andres Olivaresf2a2ddd2021-02-03 17:27:51154export function getRegisteredActionExtensions(): Array<Action> {
Andres Olivares4dba1302021-01-28 23:17:32155 return registeredActionExtensions
156 .filter(
157 action => Root.Runtime.Runtime.isDescriptorEnabled(
158 {experiment: action.experiment(), condition: action.condition()}))
159 .sort((firstAction, secondAction) => {
160 const order1 = firstAction.order() || 0;
161 const order2 = secondAction.order() || 0;
162 return order1 - order2;
163 });
Andres Olivares0e3a9e82020-12-01 14:03:20164}
165
Andres Olivares975c3022021-03-29 13:45:24166export function maybeRemoveActionExtension(actionId: string): boolean {
167 const actionIndex = registeredActionExtensions.findIndex(action => action.id() === actionId);
168 if (actionIndex < 0 || !actionIdSet.delete(actionId)) {
169 return false;
170 }
171 registeredActionExtensions.splice(actionIndex, 1);
172 return true;
173}
174
Andres Olivares4ce36a52021-01-18 18:35:05175export const enum Platforms {
Andres Olivares0e3a9e82020-12-01 14:03:20176 All = 'All platforms',
177 Mac = 'mac',
178 WindowsLinux = 'windows,linux',
179 Android = 'Android',
Andres Olivares70556d62021-02-02 19:09:01180 Windows = 'windows',
Andres Olivares0e3a9e82020-12-01 14:03:20181}
182
Kateryna Prokopenkoa72448b2021-08-31 14:16:16183export const enum Events {
184 Enabled = 'Enabled',
185 Toggled = 'Toggled',
186}
187
188export type EventTypes = {
189 [Events.Enabled]: boolean,
190 [Events.Toggled]: boolean,
Andres Olivares0e3a9e82020-12-01 14:03:20191};
192
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04193// TODO(crbug.com/1181019)
Andres Olivares0e3a9e82020-12-01 14:03:20194export const ActionCategory = {
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04195 ELEMENTS: 'Elements',
196 SCREENSHOT: 'Screenshot',
197 NETWORK: 'Network',
198 MEMORY: 'Memory',
199 JAVASCRIPT_PROFILER: 'JavaScript Profiler',
200 CONSOLE: 'Console',
201 PERFORMANCE: 'Performance',
202 MOBILE: 'Mobile',
203 SENSORS: 'Sensors',
204 HELP: 'Help',
205 INPUTS: 'Inputs',
206 LAYERS: 'Layers',
207 NAVIGATION: 'Navigation',
208 DRAWER: 'Drawer',
209 GLOBAL: 'Global',
210 RESOURCES: 'Resources',
211 BACKGROUND_SERVICES: 'Background Services',
212 SETTINGS: 'Settings',
213 DEBUGGER: 'Debugger',
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04214 SOURCES: 'Sources',
Jacky Hu4853b342022-04-29 10:47:21215 RENDERING: 'Rendering',
Andres Olivares0e3a9e82020-12-01 14:03:20216};
217
218type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
219
220export const enum IconClass {
221 LARGEICON_NODE_SEARCH = 'largeicon-node-search',
Andres Olivaresd7b94d62020-12-14 21:01:38222 LARGEICON_START_RECORDING = 'largeicon-start-recording',
223 LARGEICON_STOP_RECORDING = 'largeicon-stop-recording',
Andres Olivares37a21772020-12-22 14:10:09224 LARGEICON_REFRESH = 'largeicon-refresh',
Andres Olivares844c58d2021-01-26 13:09:07225 LARGEICON_CLEAR = 'largeicon-clear',
226 LARGEICON_VISIBILITY = 'largeicon-visibility',
Andres Olivaresfb268942021-01-26 17:12:44227 LARGEICON_PHONE = 'largeicon-phone',
Andres Olivaresa7b7b492021-01-28 16:12:37228 LARGEICON_PLAY = 'largeicon-play',
Alex Rudenkoa2ffe4a2021-05-17 13:36:31229 LARGEICON_DOWNLOAD = 'largeicon-download',
Andres Olivaresa7b7b492021-01-28 16:12:37230 LARGEICON_PAUSE = 'largeicon-pause',
231 LARGEICON_RESUME = 'largeicon-resume',
Andres Olivares27dccc02021-02-01 13:31:16232 LARGEICON_TRASH_BIN = 'largeicon-trash-bin',
Andres Olivares7b677962021-02-02 15:52:38233 LARGEICON_SETTINGS_GEAR = 'largeicon-settings-gear',
Andres Olivares7ed98072021-02-02 18:13:58234 LARGEICON_STEP_OVER = 'largeicon-step-over',
235 LARGE_ICON_STEP_INTO = 'largeicon-step-into',
236 LARGE_ICON_STEP = 'largeicon-step',
237 LARGE_ICON_STEP_OUT = 'largeicon-step-out',
238 LARGE_ICON_DEACTIVATE_BREAKPOINTS = 'largeicon-deactivate-breakpoints',
Andres Olivares70556d62021-02-02 19:09:01239 LARGE_ICON_ADD = 'largeicon-add',
Andres Olivares0e3a9e82020-12-01 14:03:20240}
241
242export const enum KeybindSet {
243 DEVTOOLS_DEFAULT = 'devToolsDefault',
244 VS_CODE = 'vsCode',
245}
246
247export interface ExtensionOption {
248 value: boolean;
Andres Olivares4ce36a52021-01-18 18:35:05249 title: () => Platform.UIString.LocalizedString;
Andres Olivares0e3a9e82020-12-01 14:03:20250 text?: string;
251}
252
253export interface Binding {
Andres Olivares4ce36a52021-01-18 18:35:05254 platform?: Platforms;
Andres Olivares0e3a9e82020-12-01 14:03:20255 shortcut: string;
256 keybindSets?: Array<KeybindSet>;
257}
258
Andres Olivares3f49f132020-12-03 13:10:27259/**
260 * The representation of an action extension to be registered.
261 */
Andres Olivares0e3a9e82020-12-01 14:03:20262export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27263 /**
264 * The unique id of an Action extension.
265 */
Andres Olivares0e3a9e82020-12-01 14:03:20266 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27267 /**
268 * The category with which the action is displayed in the UI.
269 */
Andres Olivares0e3a9e82020-12-01 14:03:20270 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27271 /**
272 * The title with which the action is displayed in the UI.
273 */
Andres Olivares4ce36a52021-01-18 18:35:05274 title?: () => Platform.UIString.LocalizedString;
Andres Olivares3f49f132020-12-03 13:10:27275 /**
276 * The type of the icon used to trigger the action.
277 */
Andres Olivares844c58d2021-01-26 13:09:07278 iconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27279 /**
280 * Whether the style of the icon toggles on interaction.
281 */
Andres Olivares844c58d2021-01-26 13:09:07282 toggledIconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27283 /**
284 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
285 */
Andres Olivares0e3a9e82020-12-01 14:03:20286 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27287 /**
288 * Words used to find an action in the Command Menu.
289 */
Andres Olivares4ce36a52021-01-18 18:35:05290 tags?: Array<() => Platform.UIString.LocalizedString>;
Andres Olivares3f49f132020-12-03 13:10:27291 /**
292 * Whether the action is toggleable.
293 */
Andres Olivares0e3a9e82020-12-01 14:03:20294 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27295 /**
Andres Olivaresa0cdb382021-01-21 15:44:33296 * Loads the class that handles the action when it is triggered. The common pattern for implementing
297 * this function relies on having the module that contains the action’s handler lazily loaded. For example:
298 * ```js
299 * let loadedElementsModule;
300 *
301 * async function loadElementsModule() {
302 *
303 * if (!loadedElementsModule) {
304 * loadedElementsModule = await import('./elements.js');
305 * }
306 * return loadedElementsModule;
307 * }
308 * UI.ActionRegistration.registerActionExtension({
309 * <...>
310 * async loadActionDelegate() {
311 * const Elements = await loadElementsModule();
312 * return Elements.ElementsPanel.ElementsActionDelegate.instance();
313 * },
314 * <...>
315 * });
316 * ```
Andres Olivares3f49f132020-12-03 13:10:27317 */
Andres Olivares0e3a9e82020-12-01 14:03:20318 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27319 /**
320 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
321 * The context of the application is described in 'flavors' that are usually views added and removed to the context
322 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
323 * When the action is supposed to be available globally, that is, it does not depend on the application to have
324 * a specific context, the value of this property should be undefined.
325 *
326 * Because the method is synchronous, context types should be already loaded when the method is invoked.
327 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
328 * return an empty array. Once the context types have been loaded, the function should return an array with all types
329 * that it depends on.
330 *
331 * The common pattern for implementing this function is relying on having the module with the corresponding context
332 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
333 *
334 * ```js
335 * let loadedElementsModule;
336 *
337 * async function loadElementsModule() {
338 *
339 * if (!loadedElementsModule) {
340 * loadedElementsModule = await import('./elements.js');
341 * }
342 * return loadedElementsModule;
343 * }
344 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
345 *
346 * if (loadedElementsModule === undefined) {
347 * return [];
348 * }
349 * return getClassCallBack(loadedElementsModule);
350 * }
351 * UI.ActionRegistration.registerActionExtension({
352 *
353 * contextTypes() {
354 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
355 * }
356 * <...>
357 * });
358 * ```
359 */
Sigurd Schneider61fc9bd2021-07-14 09:01:53360 contextTypes?: () => Array<Function>;
Andres Olivares3f49f132020-12-03 13:10:27361 /**
Andres Olivaresa0cdb382021-01-21 15:44:33362 * The descriptions for each of the two states in which a toggleable action can be.
Andres Olivares3f49f132020-12-03 13:10:27363 */
Andres Olivares0e3a9e82020-12-01 14:03:20364 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27365 /**
366 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
367 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
368 * and the keybindSet property.
369 *
370 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
371 * are flavors of the current appliaction context.
372 */
Andres Olivares0e3a9e82020-12-01 14:03:20373 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27374 /**
Andres Olivaresa0cdb382021-01-21 15:44:33375 * The name of the experiment an action is associated with. Enabling and disabling the declared
376 * experiment will enable and disable the action respectively.
Andres Olivares3f49f132020-12-03 13:10:27377 */
Andres Olivares0e3a9e82020-12-01 14:03:20378 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27379 /**
Andres Olivaresa0cdb382021-01-21 15:44:33380 * A condition represented as a string the action's availability depends on. Conditions come
381 * from the queryParamsObject defined in Runtime and just as the experiment field, they determine the availability
382 * of the setting. A condition can be negated by prepending a ‘!’ to the value of the condition
383 * property and in that case the behaviour of the action's availability will be inverted.
Andres Olivares3f49f132020-12-03 13:10:27384 */
Andres Olivares0e3a9e82020-12-01 14:03:20385 condition?: Root.Runtime.ConditionName;
Andres Olivares4dba1302021-01-28 23:17:32386 /**
387 * Used to sort actions when all registered actions are queried.
388 */
389 order?: number;
Andres Olivares0e3a9e82020-12-01 14:03:20390}