1 | // *****************************************************************************
|
2 | // Copyright (C) 2017 TypeFox and others.
|
3 | //
|
4 | // This program and the accompanying materials are made available under the
|
5 | // terms of the Eclipse Public License v. 2.0 which is available at
|
6 | // http://www.eclipse.org/legal/epl-2.0.
|
7 | //
|
8 | // This Source Code may also be made available under the following Secondary
|
9 | // Licenses when the conditions for such availability set forth in the Eclipse
|
10 | // Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
11 | // with the GNU Classpath Exception which is available at
|
12 | // https://www.gnu.org/software/classpath/license.html.
|
13 | //
|
14 | // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
15 | // *****************************************************************************
|
16 |
|
17 | import { inject, named, injectable } from 'inversify';
|
18 | import { Widget } from '@phosphor/widgets';
|
19 | import { ILogger, Emitter, Event, ContributionProvider, MaybePromise, WaitUntilEvent } from '../common';
|
20 | import stableJsonStringify = require('fast-json-stable-stringify');
|
21 |
|
22 | /* eslint-disable @typescript-eslint/no-explicit-any */
|
23 | export const WidgetFactory = Symbol('WidgetFactory');
|
24 |
|
25 | /**
|
26 | * A {@link WidgetFactory} is used to create new widgets. Factory-specific information (options) can be passed as serializable JSON data.
|
27 | * The common {@link WidgetManager} collects `WidgetFactory` contributions and delegates to the corresponding factory when
|
28 | * a widget should be created or restored. To identify widgets the `WidgetManager` uses a description composed of the factory id and the options.
|
29 | * The `WidgetFactory` does support both, synchronous and asynchronous widget creation.
|
30 | *
|
31 | * ### Example usage
|
32 | *
|
33 | * ```typescript
|
34 | * export class MyWidget extends BaseWidget {
|
35 | * }
|
36 | *
|
37 | * @injectable()
|
38 | * export class MyWidgetFactory implements WidgetFactory {
|
39 | * id = 'myWidgetFactory';
|
40 | *
|
41 | * createWidget(): MaybePromise<Widget> {
|
42 | * return new MyWidget();
|
43 | * }
|
44 | * }
|
45 | * ```
|
46 | */
|
47 | export interface WidgetFactory {
|
48 |
|
49 | /**
|
50 | * The factory id.
|
51 | */
|
52 | readonly id: string;
|
53 |
|
54 | /**
|
55 | * Creates a widget using the given options.
|
56 | * @param options factory specific information as serializable JSON data.
|
57 | *
|
58 | * @returns the newly created widget or a promise of the widget
|
59 | */
|
60 | createWidget(options?: any): MaybePromise<Widget>;
|
61 | }
|
62 |
|
63 | /**
|
64 | * Representation of the `WidgetConstructionOptions`.
|
65 | * Defines a serializable description to create widgets.
|
66 | */
|
67 | export interface WidgetConstructionOptions {
|
68 | /**
|
69 | * The id of the widget factory to use.
|
70 | */
|
71 | factoryId: string,
|
72 |
|
73 | /**
|
74 | * The widget factory specific information.
|
75 | */
|
76 | options?: any
|
77 | }
|
78 |
|
79 | /**
|
80 | * Representation of a `willCreateWidgetEvent`.
|
81 | */
|
82 | export interface WillCreateWidgetEvent extends WaitUntilEvent {
|
83 | /**
|
84 | * The widget which will be created.
|
85 | */
|
86 | readonly widget: Widget;
|
87 | /**
|
88 | * The widget factory id.
|
89 | */
|
90 | readonly factoryId: string;
|
91 | }
|
92 |
|
93 | /**
|
94 | * Representation of a `didCreateWidgetEvent`.
|
95 | */
|
96 | export interface DidCreateWidgetEvent {
|
97 | /**
|
98 | * The widget which was created.
|
99 | */
|
100 | readonly widget: Widget;
|
101 | /**
|
102 | * The widget factory id.
|
103 | */
|
104 | readonly factoryId: string;
|
105 | }
|
106 |
|
107 | /**
|
108 | * The {@link WidgetManager} is the common component responsible for creating and managing widgets. Additional widget factories
|
109 | * can be registered by using the {@link WidgetFactory} contribution point. To identify a widget, created by a factory, the factory id and
|
110 | * the creation options are used. This key is commonly referred to as `description` of the widget.
|
111 | */
|
112 | ()
|
113 | export class WidgetManager {
|
114 |
|
115 | protected _cachedFactories: Map<string, WidgetFactory>;
|
116 | protected readonly widgets = new Map<string, Widget>();
|
117 | protected readonly pendingWidgetPromises = new Map<string, MaybePromise<Widget>>();
|
118 |
|
119 | (ContributionProvider) (WidgetFactory)
|
120 | protected readonly factoryProvider: ContributionProvider<WidgetFactory>;
|
121 |
|
122 | (ILogger)
|
123 | protected readonly logger: ILogger;
|
124 |
|
125 | protected readonly onWillCreateWidgetEmitter = new Emitter<WillCreateWidgetEvent>();
|
126 | /**
|
127 | * An event can be used to participate in the widget creation.
|
128 | * Listeners may not dispose the given widget.
|
129 | */
|
130 | readonly onWillCreateWidget: Event<WillCreateWidgetEvent> = this.onWillCreateWidgetEmitter.event;
|
131 |
|
132 | protected readonly onDidCreateWidgetEmitter = new Emitter<DidCreateWidgetEvent>();
|
133 |
|
134 | readonly onDidCreateWidget: Event<DidCreateWidgetEvent> = this.onDidCreateWidgetEmitter.event;
|
135 |
|
136 | /**
|
137 | * Get the list of widgets created by the given widget factory.
|
138 | * @param factoryId the widget factory id.
|
139 | *
|
140 | * @returns the list of widgets created by the factory with the given id.
|
141 | */
|
142 | getWidgets(factoryId: string): Widget[] {
|
143 | const result: Widget[] = [];
|
144 | for (const [key, widget] of this.widgets.entries()) {
|
145 | if (this.fromKey(key).factoryId === factoryId) {
|
146 | result.push(widget);
|
147 | }
|
148 | }
|
149 | return result;
|
150 | }
|
151 |
|
152 | /**
|
153 | * Try to get the existing widget for the given description.
|
154 | * @param factoryId The widget factory id.
|
155 | * @param options The widget factory specific information.
|
156 | *
|
157 | * @returns the widget if available, else `undefined`.
|
158 | *
|
159 | * The widget is 'available' if it has been created with the same {@link factoryId} and {@link options} by the {@link WidgetManager}.
|
160 | * If the widget's creation is asynchronous, it is only available when the associated `Promise` is resolved.
|
161 | */
|
162 | tryGetWidget<T extends Widget>(factoryId: string, options?: any): T | undefined {
|
163 | const key = this.toKey({ factoryId, options });
|
164 | const existing = this.widgets.get(key);
|
165 | if (existing instanceof Widget) {
|
166 | return existing as T;
|
167 | }
|
168 | return undefined;
|
169 | }
|
170 |
|
171 | /**
|
172 | * Try to get the existing widget for the given description.
|
173 | * @param factoryId The widget factory id.
|
174 | * @param options The widget factory specific information.
|
175 | *
|
176 | * @returns A promise that resolves to the widget, if any exists. The promise may be pending, so be cautious when assuming that it will not reject.
|
177 | */
|
178 | tryGetPendingWidget<T extends Widget>(factoryId: string, options?: any): MaybePromise<T> | undefined {
|
179 | const key = this.toKey({ factoryId, options });
|
180 | return this.doGetWidget(key);
|
181 | }
|
182 |
|
183 | /**
|
184 | * Get the widget for the given description.
|
185 | * @param factoryId The widget factory id.
|
186 | * @param options The widget factory specific information.
|
187 | *
|
188 | * @returns a promise resolving to the widget if available, else `undefined`.
|
189 | */
|
190 | async getWidget<T extends Widget>(factoryId: string, options?: any): Promise<T | undefined> {
|
191 | const key = this.toKey({ factoryId, options });
|
192 | const pendingWidget = this.doGetWidget<T>(key);
|
193 | const widget = pendingWidget && await pendingWidget;
|
194 | return widget;
|
195 | }
|
196 |
|
197 | protected doGetWidget<T extends Widget>(key: string): MaybePromise<T> | undefined {
|
198 | const pendingWidget = this.widgets.get(key) ?? this.pendingWidgetPromises.get(key);
|
199 | if (pendingWidget) {
|
200 | return pendingWidget as MaybePromise<T>;
|
201 | }
|
202 | return undefined;
|
203 | }
|
204 |
|
205 | /**
|
206 | * Creates a new widget or returns the existing widget for the given description.
|
207 | * @param factoryId the widget factory id.
|
208 | * @param options the widget factory specific information.
|
209 | *
|
210 | * @returns a promise resolving to the widget.
|
211 | */
|
212 | async getOrCreateWidget<T extends Widget>(factoryId: string, options?: any): Promise<T> {
|
213 | const key = this.toKey({ factoryId, options });
|
214 | const existingWidget = this.doGetWidget<T>(key);
|
215 | if (existingWidget) {
|
216 | return existingWidget;
|
217 | }
|
218 | const factory = this.factories.get(factoryId);
|
219 | if (!factory) {
|
220 | throw Error("No widget factory '" + factoryId + "' has been registered.");
|
221 | }
|
222 | try {
|
223 | const widgetPromise = factory.createWidget(options);
|
224 | this.pendingWidgetPromises.set(key, widgetPromise);
|
225 | const widget = await widgetPromise;
|
226 | await WaitUntilEvent.fire(this.onWillCreateWidgetEmitter, { factoryId, widget });
|
227 | this.widgets.set(key, widget);
|
228 | widget.disposed.connect(() => this.widgets.delete(key));
|
229 | this.onDidCreateWidgetEmitter.fire({ factoryId, widget });
|
230 | return widget as T;
|
231 | } finally {
|
232 | this.pendingWidgetPromises.delete(key);
|
233 | }
|
234 | }
|
235 |
|
236 | /**
|
237 | * Get the widget construction options.
|
238 | * @param widget the widget.
|
239 | *
|
240 | * @returns the widget construction options if the widget was created through the manager, else `undefined`.
|
241 | */
|
242 | getDescription(widget: Widget): WidgetConstructionOptions | undefined {
|
243 | for (const [key, aWidget] of this.widgets.entries()) {
|
244 | if (aWidget === widget) {
|
245 | return this.fromKey(key);
|
246 | }
|
247 | }
|
248 | return undefined;
|
249 | }
|
250 |
|
251 | /**
|
252 | * Convert the widget construction options to string.
|
253 | * @param options the widget construction options.
|
254 | *
|
255 | * @returns the widget construction options represented as a string.
|
256 | */
|
257 | protected toKey(options: WidgetConstructionOptions): string {
|
258 | return stableJsonStringify(options);
|
259 | }
|
260 |
|
261 | /**
|
262 | * Convert the key into the widget construction options object.
|
263 | * @param key the key.
|
264 | *
|
265 | * @returns the widget construction options object.
|
266 | */
|
267 | protected fromKey(key: string): WidgetConstructionOptions {
|
268 | return JSON.parse(key);
|
269 | }
|
270 |
|
271 | protected get factories(): Map<string, WidgetFactory> {
|
272 | if (!this._cachedFactories) {
|
273 | this._cachedFactories = new Map();
|
274 | for (const factory of this.factoryProvider.getContributions()) {
|
275 | if (factory.id) {
|
276 | this._cachedFactories.set(factory.id, factory);
|
277 | } else {
|
278 | this.logger.error('Invalid ID for factory: ' + factory + ". ID was: '" + factory.id + "'.");
|
279 | }
|
280 | }
|
281 | }
|
282 | return this._cachedFactories;
|
283 | }
|
284 |
|
285 | }
|