blob: 1e9eaac1631db52e34c9361a918253eb1a301c88 [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',
Andres Olivares0e3a9e82020-12-01 14:03:20215};
216
217type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
218
219export const enum IconClass {
220 LARGEICON_NODE_SEARCH = 'largeicon-node-search',
Andres Olivaresd7b94d62020-12-14 21:01:38221 LARGEICON_START_RECORDING = 'largeicon-start-recording',
222 LARGEICON_STOP_RECORDING = 'largeicon-stop-recording',
Andres Olivares37a21772020-12-22 14:10:09223 LARGEICON_REFRESH = 'largeicon-refresh',
Andres Olivares844c58d2021-01-26 13:09:07224 LARGEICON_CLEAR = 'largeicon-clear',
225 LARGEICON_VISIBILITY = 'largeicon-visibility',
Andres Olivaresfb268942021-01-26 17:12:44226 LARGEICON_PHONE = 'largeicon-phone',
Andres Olivaresa7b7b492021-01-28 16:12:37227 LARGEICON_PLAY = 'largeicon-play',
Alex Rudenkoa2ffe4a2021-05-17 13:36:31228 LARGEICON_DOWNLOAD = 'largeicon-download',
Andres Olivaresa7b7b492021-01-28 16:12:37229 LARGEICON_PAUSE = 'largeicon-pause',
230 LARGEICON_RESUME = 'largeicon-resume',
Andres Olivares27dccc02021-02-01 13:31:16231 LARGEICON_TRASH_BIN = 'largeicon-trash-bin',
Andres Olivares7b677962021-02-02 15:52:38232 LARGEICON_SETTINGS_GEAR = 'largeicon-settings-gear',
Andres Olivares7ed98072021-02-02 18:13:58233 LARGEICON_STEP_OVER = 'largeicon-step-over',
234 LARGE_ICON_STEP_INTO = 'largeicon-step-into',
235 LARGE_ICON_STEP = 'largeicon-step',
236 LARGE_ICON_STEP_OUT = 'largeicon-step-out',
237 LARGE_ICON_DEACTIVATE_BREAKPOINTS = 'largeicon-deactivate-breakpoints',
Andres Olivares70556d62021-02-02 19:09:01238 LARGE_ICON_ADD = 'largeicon-add',
Andres Olivares0e3a9e82020-12-01 14:03:20239}
240
241export const enum KeybindSet {
242 DEVTOOLS_DEFAULT = 'devToolsDefault',
243 VS_CODE = 'vsCode',
244}
245
246export interface ExtensionOption {
247 value: boolean;
Andres Olivares4ce36a52021-01-18 18:35:05248 title: () => Platform.UIString.LocalizedString;
Andres Olivares0e3a9e82020-12-01 14:03:20249 text?: string;
250}
251
252export interface Binding {
Andres Olivares4ce36a52021-01-18 18:35:05253 platform?: Platforms;
Andres Olivares0e3a9e82020-12-01 14:03:20254 shortcut: string;
255 keybindSets?: Array<KeybindSet>;
256}
257
Andres Olivares3f49f132020-12-03 13:10:27258/**
259 * The representation of an action extension to be registered.
260 */
Andres Olivares0e3a9e82020-12-01 14:03:20261export interface ActionRegistration {
Andres Olivares3f49f132020-12-03 13:10:27262 /**
263 * The unique id of an Action extension.
264 */
Andres Olivares0e3a9e82020-12-01 14:03:20265 actionId: string;
Andres Olivares3f49f132020-12-03 13:10:27266 /**
267 * The category with which the action is displayed in the UI.
268 */
Andres Olivares0e3a9e82020-12-01 14:03:20269 category: ActionCategory;
Andres Olivares3f49f132020-12-03 13:10:27270 /**
271 * The title with which the action is displayed in the UI.
272 */
Andres Olivares4ce36a52021-01-18 18:35:05273 title?: () => Platform.UIString.LocalizedString;
Andres Olivares3f49f132020-12-03 13:10:27274 /**
275 * The type of the icon used to trigger the action.
276 */
Andres Olivares844c58d2021-01-26 13:09:07277 iconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27278 /**
279 * Whether the style of the icon toggles on interaction.
280 */
Andres Olivares844c58d2021-01-26 13:09:07281 toggledIconClass?: IconClass;
Andres Olivares3f49f132020-12-03 13:10:27282 /**
283 * Whether the class 'toolbar-toggle-with-red-color' is toggled on the icon on interaction.
284 */
Andres Olivares0e3a9e82020-12-01 14:03:20285 toggleWithRedColor?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27286 /**
287 * Words used to find an action in the Command Menu.
288 */
Andres Olivares4ce36a52021-01-18 18:35:05289 tags?: Array<() => Platform.UIString.LocalizedString>;
Andres Olivares3f49f132020-12-03 13:10:27290 /**
291 * Whether the action is toggleable.
292 */
Andres Olivares0e3a9e82020-12-01 14:03:20293 toggleable?: boolean;
Andres Olivares3f49f132020-12-03 13:10:27294 /**
Andres Olivaresa0cdb382021-01-21 15:44:33295 * Loads the class that handles the action when it is triggered. The common pattern for implementing
296 * this function relies on having the module that contains the action’s handler lazily loaded. For example:
297 * ```js
298 * let loadedElementsModule;
299 *
300 * async function loadElementsModule() {
301 *
302 * if (!loadedElementsModule) {
303 * loadedElementsModule = await import('./elements.js');
304 * }
305 * return loadedElementsModule;
306 * }
307 * UI.ActionRegistration.registerActionExtension({
308 * <...>
309 * async loadActionDelegate() {
310 * const Elements = await loadElementsModule();
311 * return Elements.ElementsPanel.ElementsActionDelegate.instance();
312 * },
313 * <...>
314 * });
315 * ```
Andres Olivares3f49f132020-12-03 13:10:27316 */
Andres Olivares0e3a9e82020-12-01 14:03:20317 loadActionDelegate?: () => Promise<ActionDelegate>;
Andres Olivares3f49f132020-12-03 13:10:27318 /**
319 * Returns the classes that represent the 'context flavors' under which the action is available for triggering.
320 * The context of the application is described in 'flavors' that are usually views added and removed to the context
321 * as the user interacts with the application (e.g when the user moves across views). (See UI.Context)
322 * When the action is supposed to be available globally, that is, it does not depend on the application to have
323 * a specific context, the value of this property should be undefined.
324 *
325 * Because the method is synchronous, context types should be already loaded when the method is invoked.
326 * In the case that an action has context types it depends on, and they haven't been loaded yet, the function should
327 * return an empty array. Once the context types have been loaded, the function should return an array with all types
328 * that it depends on.
329 *
330 * The common pattern for implementing this function is relying on having the module with the corresponding context
331 * types loaded and stored when the related 'view' extension is loaded asynchronously. As an example:
332 *
333 * ```js
334 * let loadedElementsModule;
335 *
336 * async function loadElementsModule() {
337 *
338 * if (!loadedElementsModule) {
339 * loadedElementsModule = await import('./elements.js');
340 * }
341 * return loadedElementsModule;
342 * }
343 * function maybeRetrieveContextTypes(getClassCallBack: (elementsModule: typeof Elements) => unknown[]): unknown[] {
344 *
345 * if (loadedElementsModule === undefined) {
346 * return [];
347 * }
348 * return getClassCallBack(loadedElementsModule);
349 * }
350 * UI.ActionRegistration.registerActionExtension({
351 *
352 * contextTypes() {
353 * return maybeRetrieveContextTypes(Elements => [Elements.ElementsPanel.ElementsPanel]);
354 * }
355 * <...>
356 * });
357 * ```
358 */
Sigurd Schneider61fc9bd2021-07-14 09:01:53359 contextTypes?: () => Array<Function>;
Andres Olivares3f49f132020-12-03 13:10:27360 /**
Andres Olivaresa0cdb382021-01-21 15:44:33361 * The descriptions for each of the two states in which a toggleable action can be.
Andres Olivares3f49f132020-12-03 13:10:27362 */
Andres Olivares0e3a9e82020-12-01 14:03:20363 options?: Array<ExtensionOption>;
Andres Olivares3f49f132020-12-03 13:10:27364 /**
365 * The description of the variables (e.g. platform, keys and keybind sets) under which a keyboard shortcut triggers the action.
366 * If a keybind must be available on all platforms, its 'platform' property must be undefined. The same applies to keybind sets
367 * and the keybindSet property.
368 *
369 * Keybinds also depend on the context types of their corresponding action, and so they will only be available when such context types
370 * are flavors of the current appliaction context.
371 */
Andres Olivares0e3a9e82020-12-01 14:03:20372 bindings?: Array<Binding>;
Andres Olivares3f49f132020-12-03 13:10:27373 /**
Andres Olivaresa0cdb382021-01-21 15:44:33374 * The name of the experiment an action is associated with. Enabling and disabling the declared
375 * experiment will enable and disable the action respectively.
Andres Olivares3f49f132020-12-03 13:10:27376 */
Andres Olivares0e3a9e82020-12-01 14:03:20377 experiment?: Root.Runtime.ExperimentName;
Andres Olivares3f49f132020-12-03 13:10:27378 /**
Andres Olivaresa0cdb382021-01-21 15:44:33379 * A condition represented as a string the action's availability depends on. Conditions come
380 * from the queryParamsObject defined in Runtime and just as the experiment field, they determine the availability
381 * of the setting. A condition can be negated by prepending a ‘!’ to the value of the condition
382 * property and in that case the behaviour of the action's availability will be inverted.
Andres Olivares3f49f132020-12-03 13:10:27383 */
Andres Olivares0e3a9e82020-12-01 14:03:20384 condition?: Root.Runtime.ConditionName;
Andres Olivares4dba1302021-01-28 23:17:32385 /**
386 * Used to sort actions when all registered actions are queried.
387 */
388 order?: number;
Andres Olivares0e3a9e82020-12-01 14:03:20389}