UNPKG

10.9 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { IRestorable, RestorablePool } from '@jupyterlab/statedb';
5import { IDisposable } from '@lumino/disposable';
6import { ISignal, Signal } from '@lumino/signaling';
7import { FocusTracker, Widget } from '@lumino/widgets';
8
9/**
10 * A tracker that tracks widgets.
11 *
12 * @typeparam T - The type of widget being tracked. Defaults to `Widget`.
13 */
14export interface IWidgetTracker<T extends Widget = Widget> extends IDisposable {
15 /**
16 * A signal emitted when a widget is added.
17 */
18 readonly widgetAdded: ISignal<this, T>;
19
20 /**
21 * The current widget is the most recently focused or added widget.
22 *
23 * #### Notes
24 * It is the most recently focused widget, or the most recently added
25 * widget if no widget has taken focus.
26 */
27 readonly currentWidget: T | null;
28
29 /**
30 * A signal emitted when the current instance changes.
31 *
32 * #### Notes
33 * If the last instance being tracked is disposed, `null` will be emitted.
34 */
35 readonly currentChanged: ISignal<this, T | null>;
36
37 /**
38 * The number of instances held by the tracker.
39 */
40 readonly size: number;
41
42 /**
43 * A promise that is resolved when the widget tracker has been
44 * restored from a serialized state.
45 *
46 * #### Notes
47 * Most client code will not need to use this, since they can wait
48 * for the whole application to restore. However, if an extension
49 * wants to perform actions during the application restoration, but
50 * after the restoration of another widget tracker, they can use
51 * this promise.
52 */
53 readonly restored: Promise<void>;
54
55 /**
56 * A signal emitted when a widget is updated.
57 */
58 readonly widgetUpdated: ISignal<this, T>;
59
60 /**
61 * Find the first instance in the tracker that satisfies a filter function.
62 *
63 * @param fn The filter function to call on each instance.
64 *
65 * #### Notes
66 * If nothing is found, the value returned is `undefined`.
67 */
68 find(fn: (obj: T) => boolean): T | undefined;
69
70 /**
71 * Iterate through each instance in the tracker.
72 *
73 * @param fn - The function to call on each instance.
74 */
75 forEach(fn: (obj: T) => void): void;
76
77 /**
78 * Filter the instances in the tracker based on a predicate.
79 *
80 * @param fn - The function by which to filter.
81 */
82 filter(fn: (obj: T) => boolean): T[];
83
84 /**
85 * Check if this tracker has the specified instance.
86 *
87 * @param obj - The object whose existence is being checked.
88 */
89 has(obj: Widget): boolean;
90
91 /**
92 * Inject an instance into the widget tracker without the tracker handling
93 * its restoration lifecycle.
94 *
95 * @param obj - The instance to inject into the tracker.
96 */
97 inject(obj: T): void;
98}
99
100/**
101 * A class that keeps track of widget instances on an Application shell.
102 *
103 * @typeparam T - The type of widget being tracked. Defaults to `Widget`.
104 *
105 * #### Notes
106 * The API surface area of this concrete implementation is substantially larger
107 * than the widget tracker interface it implements. The interface is intended
108 * for export by JupyterLab plugins that create widgets and have clients who may
109 * wish to keep track of newly created widgets. This class, however, can be used
110 * internally by plugins to restore state as well.
111 */
112export class WidgetTracker<T extends Widget = Widget>
113 implements IWidgetTracker<T>, IRestorable<T>
114{
115 /**
116 * Create a new widget tracker.
117 *
118 * @param options - The instantiation options for a widget tracker.
119 */
120 constructor(options: WidgetTracker.IOptions) {
121 const focus = (this._focusTracker = new FocusTracker());
122 const pool = (this._pool = new RestorablePool(options));
123
124 this.namespace = options.namespace;
125
126 focus.currentChanged.connect((_, current) => {
127 if (current.newValue !== this.currentWidget) {
128 pool.current = current.newValue;
129 }
130 }, this);
131
132 pool.added.connect((_, widget) => {
133 this._widgetAdded.emit(widget);
134 }, this);
135
136 pool.currentChanged.connect((_, widget) => {
137 // If the pool's current reference is `null` but the focus tracker has a
138 // current widget, update the pool to match the focus tracker.
139 if (widget === null && focus.currentWidget) {
140 pool.current = focus.currentWidget;
141 return;
142 }
143
144 this.onCurrentChanged(widget);
145 this._currentChanged.emit(widget);
146 }, this);
147
148 pool.updated.connect((_, widget) => {
149 this._widgetUpdated.emit(widget);
150 }, this);
151 }
152
153 /**
154 * A namespace for all tracked widgets, (e.g., `notebook`).
155 */
156 readonly namespace: string;
157
158 /**
159 * A signal emitted when the current widget changes.
160 */
161 get currentChanged(): ISignal<this, T | null> {
162 return this._currentChanged;
163 }
164
165 /**
166 * The current widget is the most recently focused or added widget.
167 *
168 * #### Notes
169 * It is the most recently focused widget, or the most recently added
170 * widget if no widget has taken focus.
171 */
172 get currentWidget(): T | null {
173 return this._pool.current || null;
174 }
175
176 /**
177 * A promise resolved when the tracker has been restored.
178 */
179 get restored(): Promise<void> {
180 if (this._deferred) {
181 return Promise.resolve();
182 } else {
183 return this._pool.restored;
184 }
185 }
186
187 /**
188 * The number of widgets held by the tracker.
189 */
190 get size(): number {
191 return this._pool.size;
192 }
193
194 /**
195 * A signal emitted when a widget is added.
196 *
197 * #### Notes
198 * This signal will only fire when a widget is added to the tracker. It will
199 * not fire if a widget is injected into the tracker.
200 */
201 get widgetAdded(): ISignal<this, T> {
202 return this._widgetAdded;
203 }
204
205 /**
206 * A signal emitted when a widget is updated.
207 */
208 get widgetUpdated(): ISignal<this, T> {
209 return this._widgetUpdated;
210 }
211
212 /**
213 * Add a new widget to the tracker.
214 *
215 * @param widget - The widget being added.
216 *
217 * #### Notes
218 * The widget passed into the tracker is added synchronously; its existence in
219 * the tracker can be checked with the `has()` method. The promise this method
220 * returns resolves after the widget has been added and saved to an underlying
221 * restoration connector, if one is available.
222 *
223 * The newly added widget becomes the current widget unless the focus tracker
224 * already had a focused widget.
225 */
226 async add(widget: T): Promise<void> {
227 this._focusTracker.add(widget);
228 await this._pool.add(widget);
229 if (!this._focusTracker.activeWidget) {
230 this._pool.current = widget;
231 }
232 }
233
234 /**
235 * Test whether the tracker is disposed.
236 */
237 get isDisposed(): boolean {
238 return this._isDisposed;
239 }
240
241 /**
242 * Dispose of the resources held by the tracker.
243 */
244 dispose(): void {
245 if (this.isDisposed) {
246 return;
247 }
248 this._isDisposed = true;
249 this._pool.dispose();
250 this._focusTracker.dispose();
251 Signal.clearData(this);
252 }
253
254 /**
255 * Find the first widget in the tracker that satisfies a filter function.
256 *
257 * @param fn The filter function to call on each widget.
258 *
259 * #### Notes
260 * If no widget is found, the value returned is `undefined`.
261 */
262 find(fn: (widget: T) => boolean): T | undefined {
263 return this._pool.find(fn);
264 }
265
266 /**
267 * Iterate through each widget in the tracker.
268 *
269 * @param fn - The function to call on each widget.
270 */
271 forEach(fn: (widget: T) => void): void {
272 return this._pool.forEach(fn);
273 }
274
275 /**
276 * Filter the widgets in the tracker based on a predicate.
277 *
278 * @param fn - The function by which to filter.
279 */
280 filter(fn: (widget: T) => boolean): T[] {
281 return this._pool.filter(fn);
282 }
283
284 /**
285 * Inject a foreign widget into the widget tracker.
286 *
287 * @param widget - The widget to inject into the tracker.
288 *
289 * #### Notes
290 * Injected widgets will not have their state saved by the tracker.
291 *
292 * The primary use case for widget injection is for a plugin that offers a
293 * sub-class of an extant plugin to have its instances share the same commands
294 * as the parent plugin (since most relevant commands will use the
295 * `currentWidget` of the parent plugin's widget tracker). In this situation,
296 * the sub-class plugin may well have its own widget tracker for layout and
297 * state restoration in addition to injecting its widgets into the parent
298 * plugin's widget tracker.
299 */
300 inject(widget: T): Promise<void> {
301 return this._pool.inject(widget);
302 }
303
304 /**
305 * Check if this tracker has the specified widget.
306 *
307 * @param widget - The widget whose existence is being checked.
308 */
309 has(widget: Widget): boolean {
310 return this._pool.has(widget as any);
311 }
312
313 /**
314 * Restore the widgets in this tracker's namespace.
315 *
316 * @param options - The configuration options that describe restoration.
317 *
318 * @returns A promise that resolves when restoration has completed.
319 *
320 * #### Notes
321 * This function should not typically be invoked by client code.
322 * Its primary use case is to be invoked by a restorer.
323 */
324 async restore(options?: IRestorable.IOptions<T>): Promise<any> {
325 const deferred = this._deferred;
326 if (deferred) {
327 this._deferred = null;
328 return this._pool.restore(deferred);
329 }
330 if (options) {
331 return this._pool.restore(options);
332 }
333 console.warn('No options provided to restore the tracker.');
334 }
335
336 /**
337 * Save the restore options for this tracker, but do not restore yet.
338 *
339 * @param options - The configuration options that describe restoration.
340 *
341 * ### Notes
342 * This function is useful when starting the shell in 'single-document' mode,
343 * to avoid restoring all useless widgets. It should not ordinarily be called
344 * by client code.
345 */
346 defer(options: IRestorable.IOptions<T>): void {
347 this._deferred = options;
348 }
349
350 /**
351 * Save the restore data for a given widget.
352 *
353 * @param widget - The widget being saved.
354 */
355 async save(widget: T): Promise<void> {
356 return this._pool.save(widget);
357 }
358
359 /**
360 * Handle the current change event.
361 *
362 * #### Notes
363 * The default implementation is a no-op.
364 */
365 protected onCurrentChanged(value: T | null): void {
366 /* no-op */
367 }
368
369 private _currentChanged = new Signal<this, T | null>(this);
370 private _deferred: IRestorable.IOptions<T> | null = null;
371 private _focusTracker: FocusTracker<T>;
372 private _pool: RestorablePool<T>;
373 private _isDisposed = false;
374 private _widgetAdded = new Signal<this, T>(this);
375 private _widgetUpdated = new Signal<this, T>(this);
376}
377
378/**
379 * A namespace for `WidgetTracker` statics.
380 */
381export namespace WidgetTracker {
382 /**
383 * The instantiation options for a widget tracker.
384 */
385 export interface IOptions {
386 /**
387 * A namespace for all tracked widgets, (e.g., `notebook`).
388 */
389 namespace: string;
390 }
391}