1 | // Copyright (c) Jupyter Development Team.
|
2 | // Distributed under the terms of the Modified BSD License.
|
3 |
|
4 | import { IRestorable, RestorablePool } from '@jupyterlab/statedb';
|
5 | import { IDisposable } from '@lumino/disposable';
|
6 | import { ISignal, Signal } from '@lumino/signaling';
|
7 | import { 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 | */
|
14 | export 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 | */
|
112 | export 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 | */
|
381 | export 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 | }
|