blob: 48c477d5df2ef1644155f6cbf13e4024716f2965 [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';
6import {ls} from '../platform/platform.js';
7import * as Root from '../root/root.js';
8
9import {Context} from './Context.js';
10
11class ActionRuntimeExtensionDescriptor extends Root.Runtime.RuntimeExtensionDescriptor {
12 iconClass?: string;
13 toggledIconClass?: string;
14 toggleWithRedColor?: boolean;
15 toggleable?: boolean;
16
17 constructor() {
18 super();
19 }
20}
21
22export interface Action extends Common.EventTarget.EventTarget {
23 id(): string;
24
25 execute(): Promise<boolean>;
26
27 icon(): string|undefined;
28
29 toggledIcon(): string|undefined;
30
31 toggleWithRedColor(): boolean;
32
33 setEnabled(_enabled: boolean): void;
34
35 enabled(): boolean;
36
37 category(): string;
38
Andres Olivares344120f2020-12-07 17:43:2839 tags(): string|void;
Andres Olivares0e3a9e82020-12-01 14:03:2040
41 toggleable(): boolean;
42
43 title(): string;
44
45 toggled(): boolean;
46
47 setToggled(_toggled: boolean): void
48}
49
50export class LegacyActionRegistration extends Common.ObjectWrapper.ObjectWrapper implements Action {
51 _extension: Root.Runtime.Extension;
52 _enabled: boolean;
53 _toggled: boolean;
54
55 constructor(extension: Root.Runtime.Extension) {
56 super();
57 this._extension = extension;
58 this._enabled = true;
59 this._toggled = false;
60 }
61
62 id(): string {
Jack Franklin01d09b02020-12-02 15:15:2063 return this.actionDescriptor().actionId || '';
Andres Olivares0e3a9e82020-12-01 14:03:2064 }
65
66 extension(): Root.Runtime.Extension {
67 return this._extension;
68 }
69
70 async execute(): Promise<boolean> {
71 if (!this._extension.canInstantiate()) {
72 return false;
73 }
74 const delegate = await this._extension.instance() as ActionDelegate;
75 const actionId = this.id();
76 return delegate.handleAction(Context.instance(), actionId);
77 }
78
79 icon(): string {
Jack Franklin01d09b02020-12-02 15:15:2080 return this.actionDescriptor().iconClass || '';
Andres Olivares0e3a9e82020-12-01 14:03:2081 }
82
83 toggledIcon(): string {
Jack Franklin01d09b02020-12-02 15:15:2084 return this.actionDescriptor().toggledIconClass || '';
Andres Olivares0e3a9e82020-12-01 14:03:2085 }
86
87 toggleWithRedColor(): boolean {
Jack Franklin01d09b02020-12-02 15:15:2088 return !!this.actionDescriptor().toggleWithRedColor;
Andres Olivares0e3a9e82020-12-01 14:03:2089 }
90
Tim van der Lipped946df02020-12-14 14:35:4991 setEnabled(enabled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:2092 if (this._enabled === enabled) {
93 return;
94 }
95
96 this._enabled = enabled;
97 this.dispatchEventToListeners(Events.Enabled, enabled);
98 }
99
100 enabled(): boolean {
101 return this._enabled;
102 }
103
104 category(): string {
Jack Franklin01d09b02020-12-02 15:15:20105 return ls`${this.actionDescriptor().category || ''}`;
Andres Olivares0e3a9e82020-12-01 14:03:20106 }
107
108 tags(): string {
Jack Franklin01d09b02020-12-02 15:15:20109 const keys = this.actionDescriptor().tags || '';
Andres Olivares0e3a9e82020-12-01 14:03:20110 // Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
111 const keyList = keys.split(',');
112 let key = '';
113 keyList.forEach(k => {
114 key += (ls(k.trim()) + '\0');
115 });
116 return key;
117 }
118
119 toggleable(): boolean {
Jack Franklin01d09b02020-12-02 15:15:20120 return !!this.actionDescriptor().toggleable;
Andres Olivares0e3a9e82020-12-01 14:03:20121 }
122
123 title(): string {
124 let title = this._extension.title() || '';
Jack Franklin01d09b02020-12-02 15:15:20125 const options = this.actionDescriptor().options;
Andres Olivares0e3a9e82020-12-01 14:03:20126 if (options) {
127 for (const pair of options) {
128 if (pair.value !== this._toggled) {
129 title = ls`${pair.title}`;
130 }
131 }
132 }
133 return title;
134 }
135
136 toggled(): boolean {
137 return this._toggled;
138 }
139
Tim van der Lipped946df02020-12-14 14:35:49140 setToggled(toggled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20141 console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id());
142 if (this._toggled === toggled) {
143 return;
144 }
145
146 this._toggled = toggled;
147 this.dispatchEventToListeners(Events.Toggled, toggled);
148 }
149
Jack Franklin01d09b02020-12-02 15:15:20150 private actionDescriptor(): ActionRuntimeExtensionDescriptor {
Andres Olivares0e3a9e82020-12-01 14:03:20151 return this._extension.descriptor() as ActionRuntimeExtensionDescriptor;
152 }
153}
154
155export interface ActionDelegate {
156 handleAction(_context: Context, _actionId: string): boolean;
157}
158
159export class PreRegisteredAction extends Common.ObjectWrapper.ObjectWrapper implements Action {
160 _enabled = true;
161 _toggled = false;
Jack Franklin01d09b02020-12-02 15:15:20162 private actionRegistration: ActionRegistration;
Andres Olivares0e3a9e82020-12-01 14:03:20163 constructor(actionRegistration: ActionRegistration) {
164 super();
Jack Franklin01d09b02020-12-02 15:15:20165 this.actionRegistration = actionRegistration;
Andres Olivares0e3a9e82020-12-01 14:03:20166 }
167
168 id(): string {
Jack Franklin01d09b02020-12-02 15:15:20169 return this.actionRegistration.actionId;
Andres Olivares0e3a9e82020-12-01 14:03:20170 }
171
172 async execute(): Promise<boolean> {
Jack Franklin01d09b02020-12-02 15:15:20173 if (!this.actionRegistration.loadActionDelegate) {
Andres Olivares0e3a9e82020-12-01 14:03:20174 return false;
175 }
Jack Franklin01d09b02020-12-02 15:15:20176 const delegate = await this.actionRegistration.loadActionDelegate();
Andres Olivares0e3a9e82020-12-01 14:03:20177 const actionId = this.id();
178 return delegate.handleAction(Context.instance(), actionId);
179 }
180
181 icon(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20182 return this.actionRegistration.iconClass;
Andres Olivares0e3a9e82020-12-01 14:03:20183 }
184
185 toggledIcon(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20186 return this.actionRegistration.toggledIconClass;
Andres Olivares0e3a9e82020-12-01 14:03:20187 }
188
189 toggleWithRedColor(): boolean {
Jack Franklin01d09b02020-12-02 15:15:20190 return !!this.actionRegistration.toggleWithRedColor;
Andres Olivares0e3a9e82020-12-01 14:03:20191 }
192
Tim van der Lipped946df02020-12-14 14:35:49193 setEnabled(enabled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20194 if (this._enabled === enabled) {
195 return;
196 }
197
198 this._enabled = enabled;
199 this.dispatchEventToListeners(Events.Enabled, enabled);
200 }
201
202 enabled(): boolean {
203 return this._enabled;
204 }
205
206 category(): string {
Jack Franklin01d09b02020-12-02 15:15:20207 return this.actionRegistration.category;
Andres Olivares0e3a9e82020-12-01 14:03:20208 }
209
Andres Olivares344120f2020-12-07 17:43:28210 tags(): string|void {
211 if (this.actionRegistration.tags) {
212 // Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
213 return this.actionRegistration.tags.join('\0');
214 }
Andres Olivares0e3a9e82020-12-01 14:03:20215 }
216
217 toggleable(): boolean {
Jack Franklin01d09b02020-12-02 15:15:20218 return !!this.actionRegistration.toggleable;
Andres Olivares0e3a9e82020-12-01 14:03:20219 }
220
221 title(): string {
Jack Franklin01d09b02020-12-02 15:15:20222 let title = this.actionRegistration.title || '';
223 const options = this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20224 if (options) {
225 // Actions with an 'options' property don't have a title field. Instead, the displayed
226 // title is taken from the 'title' property of the option that is not active. Only one of the
227 // two options can be active at a given moment and the 'toggled' property of the action along
228 // with the 'value' of the options are used to determine which one it is.
229
230 for (const pair of options) {
231 if (pair.value !== this._toggled) {
232 title = pair.title;
233 }
234 }
235 }
236 return title;
237 }
238
239 toggled(): boolean {
240 return this._toggled;
241 }
242
Tim van der Lipped946df02020-12-14 14:35:49243 setToggled(toggled: boolean): void {
Andres Olivares0e3a9e82020-12-01 14:03:20244 console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id());
245 if (this._toggled === toggled) {
246 return;
247 }
248
249 this._toggled = toggled;
250 this.dispatchEventToListeners(Events.Toggled, toggled);
251 }
252
253 options(): undefined|Array<ExtensionOption> {
Jack Franklin01d09b02020-12-02 15:15:20254 return this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20255 }
256
257 contextTypes(): undefined|Array<unknown> {
Jack Franklin01d09b02020-12-02 15:15:20258 if (this.actionRegistration.contextTypes) {
259 return this.actionRegistration.contextTypes();
Andres Olivares0e3a9e82020-12-01 14:03:20260 }
261 return undefined;
262 }
263
264 canInstantiate(): boolean {
Jack Franklin01d09b02020-12-02 15:15:20265 return !!this.actionRegistration.loadActionDelegate;
Andres Olivares0e3a9e82020-12-01 14:03:20266 }
267
268 bindings(): Array<Binding>|undefined {
Jack Franklin01d09b02020-12-02 15:15:20269 return this.actionRegistration.bindings;
Andres Olivares0e3a9e82020-12-01 14:03:20270 }
271
272 experiment(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20273 return this.actionRegistration.experiment;
Andres Olivares0e3a9e82020-12-01 14:03:20274 }
275
276 condition(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20277 return this.actionRegistration.condition;
Andres Olivares0e3a9e82020-12-01 14:03:20278 }
279}
280
281const registeredActionExtensions: Array<PreRegisteredAction> = [];
282
283const actionIdSet = new Set<string>();
284
Tim van der Lipped946df02020-12-14 14:35:49285export function registerActionExtension(registration: ActionRegistration): void {
Andres Olivares0e3a9e82020-12-01 14:03:20286 const actionId = registration.actionId;
287 if (actionIdSet.has(actionId)) {
288 throw new Error(`Duplicate Action id '${actionId}': ${new Error().stack}`);
289 }
290 actionIdSet.add(actionId);
291 registeredActionExtensions.push(new PreRegisteredAction(registration));
292}
293
294export function getRegisteredActionExtensions(): Array<PreRegisteredAction> {
295 return registeredActionExtensions.filter(
296 action =>
297 Root.Runtime.Runtime.isDescriptorEnabled({experiment: action.experiment(), condition: action.condition()}));
298}
299
300export const enum Platform {
301 All = 'All platforms',
302 Mac = 'mac',
303 WindowsLinux = 'windows,linux',
304 Android = 'Android',
305}
306
307export const Events = {
308 Enabled: Symbol('Enabled'),
309 Toggled: Symbol('Toggled'),
310};
311
312export const ActionCategory = {
313 ELEMENTS: ls`Elements`,
314 SCREENSHOT: ls`Screenshot`,
315};
316
317type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
318
319export const enum IconClass {
320 LARGEICON_NODE_SEARCH = 'largeicon-node-search',
321}
322
323export const enum KeybindSet {
324 DEVTOOLS_DEFAULT = 'devToolsDefault',
325 VS_CODE = 'vsCode',
326}
327
328export interface ExtensionOption {
329 value: boolean;
330 title: string;
331 text?: string;
332}
333
334export interface Binding {
335 platform?: Platform;
336 shortcut: string;
337 keybindSets?: Array<KeybindSet>;
338}
339
Andres Olivares3f49f132020-12-03 13:10:27340/**
341 * The representation of an action extension to be registered.
342 */
Andres Olivares0e3a9e82020-12-01 14:03:20343export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27344 /**
345 * The unique id of an Action extension.
346 */
Andres Olivares0e3a9e82020-12-01 14:03:20347 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27348 /**
349 * The category with which the action is displayed in the UI.
350 */
Andres Olivares0e3a9e82020-12-01 14:03:20351 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27352 /**
353 * The title with which the action is displayed in the UI.
354 */
Andres Olivares0e3a9e82020-12-01 14:03:20355 title?: string;
Andres Olivares3f49f132020-12-03 13:10:27356 /**
357 * The type of the icon used to trigger the action.
358 */
Andres Olivares0e3a9e82020-12-01 14:03:20359 iconClass?: string;
Andres Olivares3f49f132020-12-03 13:10:27360 /**
361 * Whether the style of the icon toggles on interaction.
362 */
Andres Olivares0e3a9e82020-12-01 14:03:20363 toggledIconClass?: string;
Andres Olivares3f49f132020-12-03 13:10:27364 /**
365 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
366 */
Andres Olivares0e3a9e82020-12-01 14:03:20367 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27368 /**
369 * Words used to find an action in the Command Menu.
370 */
Andres Olivares344120f2020-12-07 17:43:28371 tags?: Array<string>;
Andres Olivares3f49f132020-12-03 13:10:27372 /**
373 * Whether the action is toggleable.
374 */
Andres Olivares0e3a9e82020-12-01 14:03:20375 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27376 /**
377 * Loads the class that handles the action when it is triggered.
378 */
Andres Olivares0e3a9e82020-12-01 14:03:20379 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27380 /**
381 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
382 * The context of the application is described in 'flavors' that are usually views added and removed to the context
383 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
384 * When the action is supposed to be available globally, that is, it does not depend on the application to have
385 * a specific context, the value of this property should be undefined.
386 *
387 * Because the method is synchronous, context types should be already loaded when the method is invoked.
388 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
389 * return an empty array. Once the context types have been loaded, the function should return an array with all types
390 * that it depends on.
391 *
392 * The common pattern for implementing this function is relying on having the module with the corresponding context
393 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
394 *
395 * ```js
396 * let loadedElementsModule;
397 *
398 * async function loadElementsModule() {
399 *
400 * if (!loadedElementsModule) {
401 * loadedElementsModule = await import('./elements.js');
402 * }
403 * return loadedElementsModule;
404 * }
405 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
406 *
407 * if (loadedElementsModule === undefined) {
408 * return [];
409 * }
410 * return getClassCallBack(loadedElementsModule);
411 * }
412 * UI.ActionRegistration.registerActionExtension({
413 *
414 * contextTypes() {
415 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
416 * }
417 * <...>
418 * });
419 * ```
420 */
Andres Olivares0e3a9e82020-12-01 14:03:20421 contextTypes?: () => Array<unknown>;
Andres Olivares3f49f132020-12-03 13:10:27422 /**
423 * The descriptions for each of the two states in which toggleable can be.
424 */
Andres Olivares0e3a9e82020-12-01 14:03:20425 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27426 /**
427 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
428 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
429 * and the keybindSet property.
430 *
431 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
432 * are flavors of the current appliaction context.
433 */
Andres Olivares0e3a9e82020-12-01 14:03:20434 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27435 /**
436 * The name of the experiment an action is associated with.
437 */
Andres Olivares0e3a9e82020-12-01 14:03:20438 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27439 /**
440 * A condition represented as a string the action's availability depends on.
441 */
Andres Olivares0e3a9e82020-12-01 14:03:20442 condition?: Root.Runtime.ConditionName;
443}