blob: 91e087d14146610494cbd7ef955f48c13f861188 [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 {ls} from '../platform/platform.js';
8import * as Root from '../root/root.js';
9
10import {Context} from './Context.js';
11
12class ActionRuntimeExtensionDescriptor extends Root.Runtime.RuntimeExtensionDescriptor {
13 iconClass?: string;
14 toggledIconClass?: string;
15 toggleWithRedColor?: boolean;
16 toggleable?: boolean;
17
18 constructor() {
19 super();
20 }
21}
22
23export interface Action extends Common.EventTarget.EventTarget {
24 id(): string;
25
26 execute(): Promise<boolean>;
27
28 icon(): string|undefined;
29
30 toggledIcon(): string|undefined;
31
32 toggleWithRedColor(): boolean;
33
34 setEnabled(_enabled: boolean): void;
35
36 enabled(): boolean;
37
38 category(): string;
39
Andres Olivares344120f2020-12-07 17:43:2840 tags(): string|void;
Andres Olivares0e3a9e82020-12-01 14:03:2041
42 toggleable(): boolean;
43
44 title(): string;
45
46 toggled(): boolean;
47
48 setToggled(_toggled: boolean): void
49}
50
51export class LegacyActionRegistration extends Common.ObjectWrapper.ObjectWrapper implements Action {
52 _extension: Root.Runtime.Extension;
53 _enabled: boolean;
54 _toggled: boolean;
55
56 constructor(extension: Root.Runtime.Extension) {
57 super();
58 this._extension = extension;
59 this._enabled = true;
60 this._toggled = false;
61 }
62
63 id(): string {
Jack Franklin01d09b02020-12-02 15:15:2064 return this.actionDescriptor().actionId || '';
Andres Olivares0e3a9e82020-12-01 14:03:2065 }
66
67 extension(): Root.Runtime.Extension {
68 return this._extension;
69 }
70
71 async execute(): Promise<boolean> {
72 if (!this._extension.canInstantiate()) {
73 return false;
74 }
75 const delegate = await this._extension.instance() as ActionDelegate;
76 const actionId = this.id();
77 return delegate.handleAction(Context.instance(), actionId);
78 }
79
80 icon(): string {
Jack Franklin01d09b02020-12-02 15:15:2081 return this.actionDescriptor().iconClass || '';
Andres Olivares0e3a9e82020-12-01 14:03:2082 }
83
84 toggledIcon(): string {
Jack Franklin01d09b02020-12-02 15:15:2085 return this.actionDescriptor().toggledIconClass || '';
Andres Olivares0e3a9e82020-12-01 14:03:2086 }
87
88 toggleWithRedColor(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:3489 return Boolean(this.actionDescriptor().toggleWithRedColor);
Andres Olivares0e3a9e82020-12-01 14:03:2090 }
91
Tim van der Lipped946df02020-12-14 14:35:4992 setEnabled(enabled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:2093 if (this._enabled === enabled) {
94 return;
95 }
96
97 this._enabled = enabled;
98 this.dispatchEventToListeners(Events.Enabled, enabled);
99 }
100
101 enabled(): boolean {
102 return this._enabled;
103 }
104
105 category(): string {
Jack Franklin01d09b02020-12-02 15:15:20106 return ls`${this.actionDescriptor().category || ''}`;
Andres Olivares0e3a9e82020-12-01 14:03:20107 }
108
109 tags(): string {
Jack Franklin01d09b02020-12-02 15:15:20110 const keys = this.actionDescriptor().tags || '';
Andres Olivares0e3a9e82020-12-01 14:03:20111 // Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
112 const keyList = keys.split(',');
113 let key = '';
114 keyList.forEach(k => {
115 key += (ls(k.trim()) + '\0');
116 });
117 return key;
118 }
119
120 toggleable(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:34121 return Boolean(this.actionDescriptor().toggleable);
Andres Olivares0e3a9e82020-12-01 14:03:20122 }
123
124 title(): string {
125 let title = this._extension.title() || '';
Jack Franklin01d09b02020-12-02 15:15:20126 const options = this.actionDescriptor().options;
Andres Olivares0e3a9e82020-12-01 14:03:20127 if (options) {
128 for (const pair of options) {
129 if (pair.value !== this._toggled) {
130 title = ls`${pair.title}`;
131 }
132 }
133 }
134 return title;
135 }
136
137 toggled(): boolean {
138 return this._toggled;
139 }
140
Tim van der Lipped946df02020-12-14 14:35:49141 setToggled(toggled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20142 console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id());
143 if (this._toggled === toggled) {
144 return;
145 }
146
147 this._toggled = toggled;
148 this.dispatchEventToListeners(Events.Toggled, toggled);
149 }
150
Jack Franklin01d09b02020-12-02 15:15:20151 private actionDescriptor(): ActionRuntimeExtensionDescriptor {
Andres Olivares0e3a9e82020-12-01 14:03:20152 return this._extension.descriptor() as ActionRuntimeExtensionDescriptor;
153 }
154}
155
156export interface ActionDelegate {
157 handleAction(_context: Context, _actionId: string): boolean;
158}
159
160export class PreRegisteredAction extends Common.ObjectWrapper.ObjectWrapper implements Action {
161 _enabled = true;
162 _toggled = false;
Jack Franklin01d09b02020-12-02 15:15:20163 private actionRegistration: ActionRegistration;
Andres Olivares0e3a9e82020-12-01 14:03:20164 constructor(actionRegistration: ActionRegistration) {
165 super();
Jack Franklin01d09b02020-12-02 15:15:20166 this.actionRegistration = actionRegistration;
Andres Olivares0e3a9e82020-12-01 14:03:20167 }
168
169 id(): string {
Jack Franklin01d09b02020-12-02 15:15:20170 return this.actionRegistration.actionId;
Andres Olivares0e3a9e82020-12-01 14:03:20171 }
172
173 async execute(): Promise<boolean> {
Jack Franklin01d09b02020-12-02 15:15:20174 if (!this.actionRegistration.loadActionDelegate) {
Andres Olivares0e3a9e82020-12-01 14:03:20175 return false;
176 }
Jack Franklin01d09b02020-12-02 15:15:20177 const delegate = await this.actionRegistration.loadActionDelegate();
Andres Olivares0e3a9e82020-12-01 14:03:20178 const actionId = this.id();
179 return delegate.handleAction(Context.instance(), actionId);
180 }
181
182 icon(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20183 return this.actionRegistration.iconClass;
Andres Olivares0e3a9e82020-12-01 14:03:20184 }
185
186 toggledIcon(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20187 return this.actionRegistration.toggledIconClass;
Andres Olivares0e3a9e82020-12-01 14:03:20188 }
189
190 toggleWithRedColor(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:34191 return Boolean(this.actionRegistration.toggleWithRedColor);
Andres Olivares0e3a9e82020-12-01 14:03:20192 }
193
Tim van der Lipped946df02020-12-14 14:35:49194 setEnabled(enabled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20195 if (this._enabled === enabled) {
196 return;
197 }
198
199 this._enabled = enabled;
200 this.dispatchEventToListeners(Events.Enabled, enabled);
201 }
202
203 enabled(): boolean {
204 return this._enabled;
205 }
206
207 category(): string {
Jack Franklin01d09b02020-12-02 15:15:20208 return this.actionRegistration.category;
Andres Olivares0e3a9e82020-12-01 14:03:20209 }
210
Andres Olivares344120f2020-12-07 17:43:28211 tags(): string|void {
212 if (this.actionRegistration.tags) {
213 // Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
Andres Olivares4ce36a52021-01-18 18:35:05214 return this.actionRegistration.tags.map(tag => tag()).join('\0');
Andres Olivares344120f2020-12-07 17:43:28215 }
Andres Olivares0e3a9e82020-12-01 14:03:20216 }
217
218 toggleable(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:34219 return Boolean(this.actionRegistration.toggleable);
Andres Olivares0e3a9e82020-12-01 14:03:20220 }
221
222 title(): string {
Andres Olivares4ce36a52021-01-18 18:35:05223 let title = this.actionRegistration.title ? this.actionRegistration.title() : '';
Jack Franklin01d09b02020-12-02 15:15:20224 const options = this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20225 if (options) {
226 // Actions with an 'options' property don't have a title field. Instead, the displayed
227 // title is taken from the 'title' property of the option that is not active. Only one of the
228 // two options can be active at a given moment and the 'toggled' property of the action along
229 // with the 'value' of the options are used to determine which one it is.
230
231 for (const pair of options) {
232 if (pair.value !== this._toggled) {
Andres Olivares4ce36a52021-01-18 18:35:05233 title = pair.title();
Andres Olivares0e3a9e82020-12-01 14:03:20234 }
235 }
236 }
237 return title;
238 }
239
240 toggled(): boolean {
241 return this._toggled;
242 }
243
Tim van der Lipped946df02020-12-14 14:35:49244 setToggled(toggled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20245 console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id());
246 if (this._toggled === toggled) {
247 return;
248 }
249
250 this._toggled = toggled;
251 this.dispatchEventToListeners(Events.Toggled, toggled);
252 }
253
254 options(): undefined|Array<ExtensionOption> {
Jack Franklin01d09b02020-12-02 15:15:20255 return this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20256 }
257
258 contextTypes(): undefined|Array<unknown> {
Jack Franklin01d09b02020-12-02 15:15:20259 if (this.actionRegistration.contextTypes) {
260 return this.actionRegistration.contextTypes();
Andres Olivares0e3a9e82020-12-01 14:03:20261 }
262 return undefined;
263 }
264
265 canInstantiate(): boolean {
Tim van der Lippeba0e6452021-01-07 13:46:34266 return Boolean(this.actionRegistration.loadActionDelegate);
Andres Olivares0e3a9e82020-12-01 14:03:20267 }
268
269 bindings(): Array<Binding>|undefined {
Jack Franklin01d09b02020-12-02 15:15:20270 return this.actionRegistration.bindings;
Andres Olivares0e3a9e82020-12-01 14:03:20271 }
272
273 experiment(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20274 return this.actionRegistration.experiment;
Andres Olivares0e3a9e82020-12-01 14:03:20275 }
276
277 condition(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20278 return this.actionRegistration.condition;
Andres Olivares0e3a9e82020-12-01 14:03:20279 }
280}
281
282const registeredActionExtensions: Array<PreRegisteredAction> = [];
283
284const actionIdSet = new Set<string>();
285
Tim van der Lipped946df02020-12-14 14:35:49286export function registerActionExtension(registration: ActionRegistration): void {
Andres Olivares0e3a9e82020-12-01 14:03:20287 const actionId = registration.actionId;
288 if (actionIdSet.has(actionId)) {
289 throw new Error(`Duplicate Action id '${actionId}': ${new Error().stack}`);
290 }
291 actionIdSet.add(actionId);
292 registeredActionExtensions.push(new PreRegisteredAction(registration));
293}
294
295export function getRegisteredActionExtensions(): Array<PreRegisteredAction> {
296 return registeredActionExtensions.filter(
297 action =>
298 Root.Runtime.Runtime.isDescriptorEnabled({experiment: action.experiment(), condition: action.condition()}));
299}
300
Andres Olivares4ce36a52021-01-18 18:35:05301export const enum Platforms {
Andres Olivares0e3a9e82020-12-01 14:03:20302 All = 'All platforms',
303 Mac = 'mac',
304 WindowsLinux = 'windows,linux',
305 Android = 'Android',
306}
307
308export const Events = {
309 Enabled: Symbol('Enabled'),
310 Toggled: Symbol('Toggled'),
311};
312
313export const ActionCategory = {
314 ELEMENTS: ls`Elements`,
315 SCREENSHOT: ls`Screenshot`,
Andres Olivaresd7b94d62020-12-14 21:01:38316 NETWORK: ls`Network`,
Andres Olivares37a21772020-12-22 14:10:09317 MEMORY: ls`Memory`,
318 JAVASCRIPT_PROFILER: ls`JavaScript Profiler`,
Andres Olivares0e3a9e82020-12-01 14:03:20319};
320
321type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
322
323export const enum IconClass {
324 LARGEICON_NODE_SEARCH = 'largeicon-node-search',
Andres Olivaresd7b94d62020-12-14 21:01:38325 LARGEICON_START_RECORDING = 'largeicon-start-recording',
326 LARGEICON_STOP_RECORDING = 'largeicon-stop-recording',
Andres Olivares37a21772020-12-22 14:10:09327 LARGEICON_REFRESH = 'largeicon-refresh',
Andres Olivares0e3a9e82020-12-01 14:03:20328}
329
330export const enum KeybindSet {
331 DEVTOOLS_DEFAULT = 'devToolsDefault',
332 VS_CODE = 'vsCode',
333}
334
335export interface ExtensionOption {
336 value: boolean;
Andres Olivares4ce36a52021-01-18 18:35:05337 title: () => Platform.UIString.LocalizedString;
Andres Olivares0e3a9e82020-12-01 14:03:20338 text?: string;
339}
340
341export interface Binding {
Andres Olivares4ce36a52021-01-18 18:35:05342 platform?: Platforms;
Andres Olivares0e3a9e82020-12-01 14:03:20343 shortcut: string;
344 keybindSets?: Array<KeybindSet>;
345}
346
Andres Olivares3f49f132020-12-03 13:10:27347/**
348 * The representation of an action extension to be registered.
349 */
Andres Olivares0e3a9e82020-12-01 14:03:20350export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27351 /**
352 * The unique id of an Action extension.
353 */
Andres Olivares0e3a9e82020-12-01 14:03:20354 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27355 /**
356 * The category with which the action is displayed in the UI.
357 */
Andres Olivares0e3a9e82020-12-01 14:03:20358 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27359 /**
360 * The title with which the action is displayed in the UI.
361 */
Andres Olivares4ce36a52021-01-18 18:35:05362 title?: () => Platform.UIString.LocalizedString;
Andres Olivares3f49f132020-12-03 13:10:27363 /**
364 * The type of the icon used to trigger the action.
365 */
Andres Olivares0e3a9e82020-12-01 14:03:20366 iconClass?: string;
Andres Olivares3f49f132020-12-03 13:10:27367 /**
368 * Whether the style of the icon toggles on interaction.
369 */
Andres Olivares0e3a9e82020-12-01 14:03:20370 toggledIconClass?: string;
Andres Olivares3f49f132020-12-03 13:10:27371 /**
372 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
373 */
Andres Olivares0e3a9e82020-12-01 14:03:20374 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27375 /**
376 * Words used to find an action in the Command Menu.
377 */
Andres Olivares4ce36a52021-01-18 18:35:05378 tags?: Array<() => Platform.UIString.LocalizedString>;
Andres Olivares3f49f132020-12-03 13:10:27379 /**
380 * Whether the action is toggleable.
381 */
Andres Olivares0e3a9e82020-12-01 14:03:20382 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27383 /**
384 * Loads the class that handles the action when it is triggered.
385 */
Andres Olivares0e3a9e82020-12-01 14:03:20386 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27387 /**
388 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
389 * The context of the application is described in 'flavors' that are usually views added and removed to the context
390 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
391 * When the action is supposed to be available globally, that is, it does not depend on the application to have
392 * a specific context, the value of this property should be undefined.
393 *
394 * Because the method is synchronous, context types should be already loaded when the method is invoked.
395 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
396 * return an empty array. Once the context types have been loaded, the function should return an array with all types
397 * that it depends on.
398 *
399 * The common pattern for implementing this function is relying on having the module with the corresponding context
400 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
401 *
402 * ```js
403 * let loadedElementsModule;
404 *
405 * async function loadElementsModule() {
406 *
407 * if (!loadedElementsModule) {
408 * loadedElementsModule = await import('./elements.js');
409 * }
410 * return loadedElementsModule;
411 * }
412 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
413 *
414 * if (loadedElementsModule === undefined) {
415 * return [];
416 * }
417 * return getClassCallBack(loadedElementsModule);
418 * }
419 * UI.ActionRegistration.registerActionExtension({
420 *
421 * contextTypes() {
422 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
423 * }
424 * <...>
425 * });
426 * ```
427 */
Andres Olivares0e3a9e82020-12-01 14:03:20428 contextTypes?: () => Array<unknown>;
Andres Olivares3f49f132020-12-03 13:10:27429 /**
430 * The descriptions for each of the two states in which toggleable can be.
431 */
Andres Olivares0e3a9e82020-12-01 14:03:20432 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27433 /**
434 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
435 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
436 * and the keybindSet property.
437 *
438 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
439 * are flavors of the current appliaction context.
440 */
Andres Olivares0e3a9e82020-12-01 14:03:20441 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27442 /**
443 * The name of the experiment an action is associated with.
444 */
Andres Olivares0e3a9e82020-12-01 14:03:20445 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27446 /**
447 * A condition represented as a string the action's availability depends on.
448 */
Andres Olivares0e3a9e82020-12-01 14:03:20449 condition?: Root.Runtime.ConditionName;
450}