UNPKG

10.3 kBPlain TextView Raw
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
17import { inject, named, injectable } from 'inversify';
18import { Widget } from '@phosphor/widgets';
19import { ILogger, Emitter, Event, ContributionProvider, MaybePromise, WaitUntilEvent } from '../common';
20import stableJsonStringify = require('fast-json-stable-stringify');
21
22/* eslint-disable @typescript-eslint/no-explicit-any */
23export 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 */
47export 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 */
67export 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 */
82export 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 */
96export 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@injectable()
113export 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 @inject(ContributionProvider) @named(WidgetFactory)
120 protected readonly factoryProvider: ContributionProvider<WidgetFactory>;
121
122 @inject(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}