blob: 2ed727efe0c5c9fdfbb8504f3c561ec1acf513f6 [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
39 tags(): string|undefined;
40
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
91 setEnabled(enabled: boolean) {
92 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
140 setToggled(toggled: boolean) {
141 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
193 setEnabled(enabled: boolean) {
194 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
210 tags(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20211 return this.actionRegistration.tags;
Andres Olivares0e3a9e82020-12-01 14:03:20212 }
213
214 toggleable(): boolean {
Jack Franklin01d09b02020-12-02 15:15:20215 return !!this.actionRegistration.toggleable;
Andres Olivares0e3a9e82020-12-01 14:03:20216 }
217
218 title(): string {
Jack Franklin01d09b02020-12-02 15:15:20219 let title = this.actionRegistration.title || '';
220 const options = this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20221 if (options) {
222 // Actions with an 'options' property don't have a title field. Instead, the displayed
223 // title is taken from the 'title' property of the option that is not active. Only one of the
224 // two options can be active at a given moment and the 'toggled' property of the action along
225 // with the 'value' of the options are used to determine which one it is.
226
227 for (const pair of options) {
228 if (pair.value !== this._toggled) {
229 title = pair.title;
230 }
231 }
232 }
233 return title;
234 }
235
236 toggled(): boolean {
237 return this._toggled;
238 }
239
240 setToggled(toggled: boolean) {
241 console.assert(this.toggleable(), 'Shouldn\'t be toggling an untoggleable action', this.id());
242 if (this._toggled === toggled) {
243 return;
244 }
245
246 this._toggled = toggled;
247 this.dispatchEventToListeners(Events.Toggled, toggled);
248 }
249
250 options(): undefined|Array<ExtensionOption> {
Jack Franklin01d09b02020-12-02 15:15:20251 return this.actionRegistration.options;
Andres Olivares0e3a9e82020-12-01 14:03:20252 }
253
254 contextTypes(): undefined|Array<unknown> {
Jack Franklin01d09b02020-12-02 15:15:20255 if (this.actionRegistration.contextTypes) {
256 return this.actionRegistration.contextTypes();
Andres Olivares0e3a9e82020-12-01 14:03:20257 }
258 return undefined;
259 }
260
261 canInstantiate(): boolean {
Jack Franklin01d09b02020-12-02 15:15:20262 return !!this.actionRegistration.loadActionDelegate;
Andres Olivares0e3a9e82020-12-01 14:03:20263 }
264
265 bindings(): Array<Binding>|undefined {
Jack Franklin01d09b02020-12-02 15:15:20266 return this.actionRegistration.bindings;
Andres Olivares0e3a9e82020-12-01 14:03:20267 }
268
269 experiment(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20270 return this.actionRegistration.experiment;
Andres Olivares0e3a9e82020-12-01 14:03:20271 }
272
273 condition(): string|undefined {
Jack Franklin01d09b02020-12-02 15:15:20274 return this.actionRegistration.condition;
Andres Olivares0e3a9e82020-12-01 14:03:20275 }
276}
277
278const registeredActionExtensions: Array<PreRegisteredAction> = [];
279
280const actionIdSet = new Set<string>();
281
282export function registerActionExtension(registration: ActionRegistration) {
283 const actionId = registration.actionId;
284 if (actionIdSet.has(actionId)) {
285 throw new Error(`Duplicate Action id '${actionId}': ${new Error().stack}`);
286 }
287 actionIdSet.add(actionId);
288 registeredActionExtensions.push(new PreRegisteredAction(registration));
289}
290
291export function getRegisteredActionExtensions(): Array<PreRegisteredAction> {
292 return registeredActionExtensions.filter(
293 action =>
294 Root.Runtime.Runtime.isDescriptorEnabled({experiment: action.experiment(), condition: action.condition()}));
295}
296
297export const enum Platform {
298 All = 'All platforms',
299 Mac = 'mac',
300 WindowsLinux = 'windows,linux',
301 Android = 'Android',
302}
303
304export const Events = {
305 Enabled: Symbol('Enabled'),
306 Toggled: Symbol('Toggled'),
307};
308
309export const ActionCategory = {
310 ELEMENTS: ls`Elements`,
311 SCREENSHOT: ls`Screenshot`,
312};
313
314type ActionCategory = typeof ActionCategory[keyof typeof ActionCategory];
315
316export const enum IconClass {
317 LARGEICON_NODE_SEARCH = 'largeicon-node-search',
318}
319
320export const enum KeybindSet {
321 DEVTOOLS_DEFAULT = 'devToolsDefault',
322 VS_CODE = 'vsCode',
323}
324
325export interface ExtensionOption {
326 value: boolean;
327 title: string;
328 text?: string;
329}
330
331export interface Binding {
332 platform?: Platform;
333 shortcut: string;
334 keybindSets?: Array<KeybindSet>;
335}
336
337export interface ActionRegistration {
338 actionId: string;
339 category: ActionCategory;
340 title?: string;
341 iconClass?: string;
342 toggledIconClass?: string;
343 toggleWithRedColor?: boolean;
344 tags?: string;
345 toggleable?: boolean;
346 loadActionDelegate?: () => Promise<ActionDelegate>;
347 contextTypes?: () => Array<unknown>;
348 options?: Array<ExtensionOption>;
349 bindings?: Array<Binding>;
350 experiment?: Root.Runtime.ExperimentName;
351 condition?: Root.Runtime.ConditionName;
352}