blob: dbdae6c883d24e9d3b70a6627d0ecba81cc9edc7 [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
5import * as Common from '../common/common.js';
Andres Olivares4ce36a52021-01-18 18:35:056import * as Platform from '../platform/platform.js';
Andres Olivares0e3a9e82020-12-01 14:03:207import * as Root from '../root/root.js';
8
9import {Context} from './Context.js';
10
Andres Olivares0e3a9e82020-12-01 14:03:2011export interface ActionDelegate {
12 handleAction(_context: Context, _actionId: string): boolean;
13}
14
Andres Olivaresf2a2ddd2021-02-03 17:27:5115export class Action extends Common.ObjectWrapper.ObjectWrapper {
Andres Olivares0e3a9e82020-12-01 14:03:2016 _enabled = true;
17 _toggled = 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 {
Andres Olivares0e3a9e82020-12-01 14:03:2050 if (this._enabled === enabled) {
51 return;
52 }
53
54 this._enabled = enabled;
55 this.dispatchEventToListeners(Events.Enabled, enabled);
56 }
57
58 enabled(): boolean {
59 return this._enabled;
60 }
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) {
87 if (pair.value !== this._toggled) {
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 {
96 return this._toggled;
97 }
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());
101 if (this._toggled === toggled) {
102 return;
103 }
104
105 this._toggled = toggled;
106 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
113 contextTypes(): undefined|Array<unknown> {
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
183export const Events = {
184 Enabled: Symbol('Enabled'),
185 Toggled: Symbol('Toggled'),
186};
187
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04188// TODO(crbug.com/1181019)
Andres Olivares0e3a9e82020-12-01 14:03:20189export const ActionCategory = {
Vidal Guillermo Diazleal Ortega9b0bc5b2021-02-22 22:12:04190 ELEMENTS: 'Elements',
191 SCREENSHOT: 'Screenshot',
192 NETWORK: 'Network',
193 MEMORY: 'Memory',
194 JAVASCRIPT_PROFILER: 'JavaScript Profiler',
195 CONSOLE: 'Console',
196 PERFORMANCE: 'Performance',
197 MOBILE: 'Mobile',
198 SENSORS: 'Sensors',
199 HELP: 'Help',
200 INPUTS: 'Inputs',
201 LAYERS: 'Layers',
202 NAVIGATION: 'Navigation',
203 DRAWER: 'Drawer',
204 GLOBAL: 'Global',
205 RESOURCES: 'Resources',
206 BACKGROUND_SERVICES: 'Background Services',
207 SETTINGS: 'Settings',
208 DEBUGGER: 'Debugger',
209 RECORDER: 'Recorder',
210 SOURCES: 'Sources',
Andres Olivares0e3a9e82020-12-01 14:03:20211};
212
213type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
214
215export const enum IconClass {
216 LARGEICON_NODE_SEARCH = 'largeicon-node-search',
Andres Olivaresd7b94d62020-12-14 21:01:38217 LARGEICON_START_RECORDING = 'largeicon-start-recording',
218 LARGEICON_STOP_RECORDING = 'largeicon-stop-recording',
Andres Olivares37a21772020-12-22 14:10:09219 LARGEICON_REFRESH = 'largeicon-refresh',
Andres Olivares844c58d2021-01-26 13:09:07220 LARGEICON_CLEAR = 'largeicon-clear',
221 LARGEICON_VISIBILITY = 'largeicon-visibility',
Andres Olivaresfb268942021-01-26 17:12:44222 LARGEICON_PHONE = 'largeicon-phone',
Andres Olivaresa7b7b492021-01-28 16:12:37223 LARGEICON_PLAY = 'largeicon-play',
224 LARGEICON_PAUSE = 'largeicon-pause',
225 LARGEICON_RESUME = 'largeicon-resume',
Andres Olivares27dccc02021-02-01 13:31:16226 LARGEICON_TRASH_BIN = 'largeicon-trash-bin',
Andres Olivares7b677962021-02-02 15:52:38227 LARGEICON_SETTINGS_GEAR = 'largeicon-settings-gear',
Andres Olivares7ed98072021-02-02 18:13:58228 LARGEICON_STEP_OVER = 'largeicon-step-over',
229 LARGE_ICON_STEP_INTO = 'largeicon-step-into',
230 LARGE_ICON_STEP = 'largeicon-step',
231 LARGE_ICON_STEP_OUT = 'largeicon-step-out',
232 LARGE_ICON_DEACTIVATE_BREAKPOINTS = 'largeicon-deactivate-breakpoints',
Andres Olivares70556d62021-02-02 19:09:01233 LARGE_ICON_ADD = 'largeicon-add',
Andres Olivares0e3a9e82020-12-01 14:03:20234}
235
236export const enum KeybindSet {
237 DEVTOOLS_DEFAULT = 'devToolsDefault',
238 VS_CODE = 'vsCode',
239}
240
241export interface ExtensionOption {
242 value: boolean;
Andres Olivares4ce36a52021-01-18 18:35:05243 title: () => Platform.UIString.LocalizedString;
Andres Olivares0e3a9e82020-12-01 14:03:20244 text?: string;
245}
246
247export interface Binding {
Andres Olivares4ce36a52021-01-18 18:35:05248 platform?: Platforms;
Andres Olivares0e3a9e82020-12-01 14:03:20249 shortcut: string;
250 keybindSets?: Array<KeybindSet>;
251}
252
Andres Olivares3f49f132020-12-03 13:10:27253/**
254 * The representation of an action extension to be registered.
255 */
Andres Olivares0e3a9e82020-12-01 14:03:20256export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27257 /**
258 * The unique id of an Action extension.
259 */
Andres Olivares0e3a9e82020-12-01 14:03:20260 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27261 /**
262 * The category with which the action is displayed in the UI.
263 */
Andres Olivares0e3a9e82020-12-01 14:03:20264 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27265 /**
266 * The title with which the action is displayed in the UI.
267 */
Andres Olivares4ce36a52021-01-18 18:35:05268 title?: () => Platform.UIString.LocalizedString;
Andres Olivares3f49f132020-12-03 13:10:27269 /**
270 * The type of the icon used to trigger the action.
271 */
Andres Olivares844c58d2021-01-26 13:09:07272 iconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27273 /**
274 * Whether the style of the icon toggles on interaction.
275 */
Andres Olivares844c58d2021-01-26 13:09:07276 toggledIconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27277 /**
278 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
279 */
Andres Olivares0e3a9e82020-12-01 14:03:20280 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27281 /**
282 * Words used to find an action in the Command Menu.
283 */
Andres Olivares4ce36a52021-01-18 18:35:05284 tags?: Array<() => Platform.UIString.LocalizedString>;
Andres Olivares3f49f132020-12-03 13:10:27285 /**
286 * Whether the action is toggleable.
287 */
Andres Olivares0e3a9e82020-12-01 14:03:20288 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27289 /**
Andres Olivaresa0cdb382021-01-21 15:44:33290 * Loads the class that handles the action when it is triggered. The common pattern for implementing
291 * this function relies on having the module that contains the action’s handler lazily loaded. For example:
292 * ```js
293 * let loadedElementsModule;
294 *
295 * async function loadElementsModule() {
296 *
297 * if (!loadedElementsModule) {
298 * loadedElementsModule = await import('./elements.js');
299 * }
300 * return loadedElementsModule;
301 * }
302 * UI.ActionRegistration.registerActionExtension({
303 * <...>
304 * async loadActionDelegate() {
305 * const Elements = await loadElementsModule();
306 * return Elements.ElementsPanel.ElementsActionDelegate.instance();
307 * },
308 * <...>
309 * });
310 * ```
Andres Olivares3f49f132020-12-03 13:10:27311 */
Andres Olivares0e3a9e82020-12-01 14:03:20312 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27313 /**
314 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
315 * The context of the application is described in 'flavors' that are usually views added and removed to the context
316 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
317 * When the action is supposed to be available globally, that is, it does not depend on the application to have
318 * a specific context, the value of this property should be undefined.
319 *
320 * Because the method is synchronous, context types should be already loaded when the method is invoked.
321 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
322 * return an empty array. Once the context types have been loaded, the function should return an array with all types
323 * that it depends on.
324 *
325 * The common pattern for implementing this function is relying on having the module with the corresponding context
326 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
327 *
328 * ```js
329 * let loadedElementsModule;
330 *
331 * async function loadElementsModule() {
332 *
333 * if (!loadedElementsModule) {
334 * loadedElementsModule = await import('./elements.js');
335 * }
336 * return loadedElementsModule;
337 * }
338 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
339 *
340 * if (loadedElementsModule === undefined) {
341 * return [];
342 * }
343 * return getClassCallBack(loadedElementsModule);
344 * }
345 * UI.ActionRegistration.registerActionExtension({
346 *
347 * contextTypes() {
348 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
349 * }
350 * <...>
351 * });
352 * ```
353 */
Andres Olivares0e3a9e82020-12-01 14:03:20354 contextTypes?: () => Array<unknown>;
Andres Olivares3f49f132020-12-03 13:10:27355 /**
Andres Olivaresa0cdb382021-01-21 15:44:33356 * The descriptions for each of the two states in which a toggleable action can be.
Andres Olivares3f49f132020-12-03 13:10:27357 */
Andres Olivares0e3a9e82020-12-01 14:03:20358 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27359 /**
360 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
361 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
362 * and the keybindSet property.
363 *
364 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
365 * are flavors of the current appliaction context.
366 */
Andres Olivares0e3a9e82020-12-01 14:03:20367 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27368 /**
Andres Olivaresa0cdb382021-01-21 15:44:33369 * The name of the experiment an action is associated with. Enabling and disabling the declared
370 * experiment will enable and disable the action respectively.
Andres Olivares3f49f132020-12-03 13:10:27371 */
Andres Olivares0e3a9e82020-12-01 14:03:20372 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27373 /**
Andres Olivaresa0cdb382021-01-21 15:44:33374 * A condition represented as a string the action's availability depends on. Conditions come
375 * from the queryParamsObject defined in Runtime and just as the experiment field, they determine the availability
376 * of the setting. A condition can be negated by prepending a ‘!’ to the value of the condition
377 * property and in that case the behaviour of the action's availability will be inverted.
Andres Olivares3f49f132020-12-03 13:10:27378 */
Andres Olivares0e3a9e82020-12-01 14:03:20379 condition?: Root.Runtime.ConditionName;
Andres Olivares4dba1302021-01-28 23:17:32380 /**
381 * Used to sort actions when all registered actions are queried.
382 */
383 order?: number;
Andres Olivares0e3a9e82020-12-01 14:03:20384}