1 | /* -----------------------------------------------------------------------------
|
2 | | Copyright (c) Jupyter Development Team.
|
3 | | Distributed under the terms of the Modified BSD License.
|
4 | |----------------------------------------------------------------------------*/
|
5 | import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils';
|
6 | import { AttachedProperty } from '@lumino/properties';
|
7 | /**
|
8 | * The layout restorer token.
|
9 | */
|
10 | export const ILayoutRestorer = new Token('@jupyterlab/application:ILayoutRestorer');
|
11 | /**
|
12 | * The data connector key for restorer data.
|
13 | */
|
14 | const KEY = 'layout-restorer:data';
|
15 | /**
|
16 | * The default implementation of a layout restorer.
|
17 | *
|
18 | * #### Notes
|
19 | * The lifecycle for state restoration is subtle. The sequence of events is:
|
20 | *
|
21 | * 1. The layout restorer plugin is instantiated and makes a `fetch` call to
|
22 | * the data connector that stores the layout restoration data. The `fetch`
|
23 | * call returns a promise that resolves in step 6, below.
|
24 | *
|
25 | * 2. Other plugins that care about state restoration require the layout
|
26 | * restorer as a dependency.
|
27 | *
|
28 | * 3. As each load-time plugin initializes (which happens before the front-end
|
29 | * application has `started`), it instructs the layout restorer whether
|
30 | * the restorer ought to `restore` its widgets by passing in its widget
|
31 | * tracker.
|
32 | * Alternatively, a plugin that does not require its own widget tracker
|
33 | * (because perhaps it only creates a single widget, like a command palette),
|
34 | * can simply `add` its widget along with a persistent unique name to the
|
35 | * layout restorer so that its layout state can be restored when the lab
|
36 | * application restores.
|
37 | *
|
38 | * 4. After all the load-time plugins have finished initializing, the front-end
|
39 | * application `started` promise will resolve. This is the `first`
|
40 | * promise that the layout restorer waits for. By this point, all of the
|
41 | * plugins that care about restoration will have instructed the layout
|
42 | * restorer to `restore` their widget trackers.
|
43 | *
|
44 | * 5. The layout restorer will then instruct each plugin's widget tracker
|
45 | * to restore its state and reinstantiate whichever widgets it wants. The
|
46 | * tracker returns a promise to the layout restorer that resolves when it
|
47 | * has completed restoring the tracked widgets it cares about.
|
48 | *
|
49 | * 6. As each widget tracker finishes restoring the widget instances it cares
|
50 | * about, it resolves the promise that was returned to the layout restorer
|
51 | * (in step 5). After all of the promises that the restorer is awaiting have
|
52 | * settled, the restorer then resolves the outstanding `fetch` promise
|
53 | * (from step 1) and hands off a layout state object to the application
|
54 | * shell's `restoreLayout` method for restoration.
|
55 | *
|
56 | * 7. Once the application shell has finished restoring the layout, the
|
57 | * JupyterLab application's `restored` promise is resolved.
|
58 | *
|
59 | * Of particular note are steps 5 and 6: since data restoration of plugins
|
60 | * is accomplished by executing commands, the command that is used to restore
|
61 | * the data of each plugin must return a promise that only resolves when the
|
62 | * widget has been created and added to the plugin's widget tracker.
|
63 | */
|
64 | export class LayoutRestorer {
|
65 | /**
|
66 | * Create a layout restorer.
|
67 | */
|
68 | constructor(options) {
|
69 | this._firstDone = false;
|
70 | this._promisesDone = false;
|
71 | this._promises = [];
|
72 | this._restored = new PromiseDelegate();
|
73 | this._trackers = new Set();
|
74 | this._widgets = new Map();
|
75 | this._connector = options.connector;
|
76 | this._first = options.first;
|
77 | this._registry = options.registry;
|
78 | void this._first
|
79 | .then(() => {
|
80 | this._firstDone = true;
|
81 | })
|
82 | .then(() => Promise.all(this._promises))
|
83 | .then(() => {
|
84 | this._promisesDone = true;
|
85 | // Release the tracker set.
|
86 | this._trackers.clear();
|
87 | })
|
88 | .then(() => {
|
89 | this._restored.resolve(void 0);
|
90 | });
|
91 | }
|
92 | /**
|
93 | * A promise resolved when the layout restorer is ready to receive signals.
|
94 | */
|
95 | get restored() {
|
96 | return this._restored.promise;
|
97 | }
|
98 | /**
|
99 | * Add a widget to be tracked by the layout restorer.
|
100 | */
|
101 | add(widget, name) {
|
102 | Private.nameProperty.set(widget, name);
|
103 | this._widgets.set(name, widget);
|
104 | widget.disposed.connect(this._onWidgetDisposed, this);
|
105 | }
|
106 | /**
|
107 | * Fetch the layout state for the application.
|
108 | *
|
109 | * #### Notes
|
110 | * Fetching the layout relies on all widget restoration to be complete, so
|
111 | * calls to `fetch` are guaranteed to return after restoration is complete.
|
112 | */
|
113 | async fetch() {
|
114 | const blank = {
|
115 | fresh: true,
|
116 | mainArea: null,
|
117 | downArea: null,
|
118 | leftArea: null,
|
119 | rightArea: null,
|
120 | relativeSizes: null
|
121 | };
|
122 | const layout = this._connector.fetch(KEY);
|
123 | try {
|
124 | const [data] = await Promise.all([layout, this.restored]);
|
125 | if (!data) {
|
126 | return blank;
|
127 | }
|
128 | const { main, down, left, right, relativeSizes } = data;
|
129 | // If any data exists, then this is not a fresh session.
|
130 | const fresh = false;
|
131 | // Rehydrate main area.
|
132 | const mainArea = this._rehydrateMainArea(main);
|
133 | // Rehydrate down area.
|
134 | const downArea = this._rehydrateDownArea(down);
|
135 | // Rehydrate left area.
|
136 | const leftArea = this._rehydrateSideArea(left);
|
137 | // Rehydrate right area.
|
138 | const rightArea = this._rehydrateSideArea(right);
|
139 | return {
|
140 | fresh,
|
141 | mainArea,
|
142 | downArea,
|
143 | leftArea,
|
144 | rightArea,
|
145 | relativeSizes: relativeSizes || null
|
146 | };
|
147 | }
|
148 | catch (error) {
|
149 | return blank;
|
150 | }
|
151 | }
|
152 | /**
|
153 | * Restore the widgets of a particular widget tracker.
|
154 | *
|
155 | * @param tracker - The widget tracker whose widgets will be restored.
|
156 | *
|
157 | * @param options - The restoration options.
|
158 | */
|
159 | restore(tracker, options) {
|
160 | const warning = 'restore() can only be called before `first` has resolved.';
|
161 | if (this._firstDone) {
|
162 | console.warn(warning);
|
163 | return Promise.reject(warning);
|
164 | }
|
165 | const { namespace } = tracker;
|
166 | if (this._trackers.has(namespace)) {
|
167 | const warning = `A tracker namespaced ${namespace} was already restored.`;
|
168 | console.warn(warning);
|
169 | return Promise.reject(warning);
|
170 | }
|
171 | const { args, command, name, when } = options;
|
172 | // Add the tracker to the private trackers collection.
|
173 | this._trackers.add(namespace);
|
174 | // Whenever a new widget is added to the tracker, record its name.
|
175 | tracker.widgetAdded.connect((_, widget) => {
|
176 | const widgetName = name(widget);
|
177 | if (widgetName) {
|
178 | this.add(widget, `${namespace}:${widgetName}`);
|
179 | }
|
180 | }, this);
|
181 | // Whenever a widget is updated, get its new name.
|
182 | tracker.widgetUpdated.connect((_, widget) => {
|
183 | const widgetName = name(widget);
|
184 | if (widgetName) {
|
185 | const name = `${namespace}:${widgetName}`;
|
186 | Private.nameProperty.set(widget, name);
|
187 | this._widgets.set(name, widget);
|
188 | }
|
189 | });
|
190 | const first = this._first;
|
191 | const promise = tracker
|
192 | .restore({
|
193 | args: args || (() => JSONExt.emptyObject),
|
194 | command,
|
195 | connector: this._connector,
|
196 | name,
|
197 | registry: this._registry,
|
198 | when: when ? [first].concat(when) : first
|
199 | })
|
200 | .catch(error => {
|
201 | console.error(error);
|
202 | });
|
203 | this._promises.push(promise);
|
204 | return promise;
|
205 | }
|
206 | /**
|
207 | * Save the layout state for the application.
|
208 | */
|
209 | save(data) {
|
210 | // If there are promises that are unresolved, bail.
|
211 | if (!this._promisesDone) {
|
212 | const warning = 'save() was called prematurely.';
|
213 | console.warn(warning);
|
214 | return Promise.reject(warning);
|
215 | }
|
216 | const dehydrated = {};
|
217 | dehydrated.main = this._dehydrateMainArea(data.mainArea);
|
218 | dehydrated.down = this._dehydrateDownArea(data.downArea);
|
219 | dehydrated.left = this._dehydrateSideArea(data.leftArea);
|
220 | dehydrated.right = this._dehydrateSideArea(data.rightArea);
|
221 | dehydrated.relativeSizes = data.relativeSizes;
|
222 | return this._connector.save(KEY, dehydrated);
|
223 | }
|
224 | /**
|
225 | * Dehydrate a main area description into a serializable object.
|
226 | */
|
227 | _dehydrateMainArea(area) {
|
228 | if (!area) {
|
229 | return null;
|
230 | }
|
231 | return Private.serializeMain(area);
|
232 | }
|
233 | /**
|
234 | * Reydrate a serialized main area description object.
|
235 | *
|
236 | * #### Notes
|
237 | * This function consumes data that can become corrupted, so it uses type
|
238 | * coercion to guarantee the dehydrated object is safely processed.
|
239 | */
|
240 | _rehydrateMainArea(area) {
|
241 | if (!area) {
|
242 | return null;
|
243 | }
|
244 | return Private.deserializeMain(area, this._widgets);
|
245 | }
|
246 | /**
|
247 | * Dehydrate a down area description into a serializable object.
|
248 | */
|
249 | _dehydrateDownArea(area) {
|
250 | if (!area) {
|
251 | return null;
|
252 | }
|
253 | const dehydrated = {
|
254 | size: area.size
|
255 | };
|
256 | if (area.currentWidget) {
|
257 | const current = Private.nameProperty.get(area.currentWidget);
|
258 | if (current) {
|
259 | dehydrated.current = current;
|
260 | }
|
261 | }
|
262 | if (area.widgets) {
|
263 | dehydrated.widgets = area.widgets
|
264 | .map(widget => Private.nameProperty.get(widget))
|
265 | .filter(name => !!name);
|
266 | }
|
267 | return dehydrated;
|
268 | }
|
269 | /**
|
270 | * Reydrate a serialized side area description object.
|
271 | *
|
272 | * #### Notes
|
273 | * This function consumes data that can become corrupted, so it uses type
|
274 | * coercion to guarantee the dehydrated object is safely processed.
|
275 | */
|
276 | _rehydrateDownArea(area) {
|
277 | var _a;
|
278 | if (!area) {
|
279 | return { currentWidget: null, size: 0.0, widgets: null };
|
280 | }
|
281 | const internal = this._widgets;
|
282 | const currentWidget = area.current && internal.has(`${area.current}`)
|
283 | ? internal.get(`${area.current}`)
|
284 | : null;
|
285 | const widgets = !Array.isArray(area.widgets)
|
286 | ? null
|
287 | : area.widgets
|
288 | .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null)
|
289 | .filter(widget => !!widget);
|
290 | return {
|
291 | currentWidget: currentWidget,
|
292 | size: (_a = area.size) !== null && _a !== void 0 ? _a : 0.0,
|
293 | widgets: widgets
|
294 | };
|
295 | }
|
296 | /**
|
297 | * Dehydrate a side area description into a serializable object.
|
298 | */
|
299 | _dehydrateSideArea(area) {
|
300 | if (!area) {
|
301 | return null;
|
302 | }
|
303 | const dehydrated = { collapsed: area.collapsed };
|
304 | if (area.currentWidget) {
|
305 | const current = Private.nameProperty.get(area.currentWidget);
|
306 | if (current) {
|
307 | dehydrated.current = current;
|
308 | }
|
309 | }
|
310 | if (area.widgets) {
|
311 | dehydrated.widgets = area.widgets
|
312 | .map(widget => Private.nameProperty.get(widget))
|
313 | .filter(name => !!name);
|
314 | }
|
315 | return dehydrated;
|
316 | }
|
317 | /**
|
318 | * Reydrate a serialized side area description object.
|
319 | *
|
320 | * #### Notes
|
321 | * This function consumes data that can become corrupted, so it uses type
|
322 | * coercion to guarantee the dehydrated object is safely processed.
|
323 | */
|
324 | _rehydrateSideArea(area) {
|
325 | var _a;
|
326 | if (!area) {
|
327 | return { collapsed: true, currentWidget: null, widgets: null };
|
328 | }
|
329 | const internal = this._widgets;
|
330 | const collapsed = (_a = area.collapsed) !== null && _a !== void 0 ? _a : false;
|
331 | const currentWidget = area.current && internal.has(`${area.current}`)
|
332 | ? internal.get(`${area.current}`)
|
333 | : null;
|
334 | const widgets = !Array.isArray(area.widgets)
|
335 | ? null
|
336 | : area.widgets
|
337 | .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null)
|
338 | .filter(widget => !!widget);
|
339 | return {
|
340 | collapsed,
|
341 | currentWidget: currentWidget,
|
342 | widgets: widgets
|
343 | };
|
344 | }
|
345 | /**
|
346 | * Handle a widget disposal.
|
347 | */
|
348 | _onWidgetDisposed(widget) {
|
349 | const name = Private.nameProperty.get(widget);
|
350 | this._widgets.delete(name);
|
351 | }
|
352 | }
|
353 | /*
|
354 | * A namespace for private data.
|
355 | */
|
356 | var Private;
|
357 | (function (Private) {
|
358 | /**
|
359 | * An attached property for a widget's ID in the serialized restore data.
|
360 | */
|
361 | Private.nameProperty = new AttachedProperty({
|
362 | name: 'name',
|
363 | create: owner => ''
|
364 | });
|
365 | /**
|
366 | * Serialize individual areas within the main area.
|
367 | */
|
368 | function serializeArea(area) {
|
369 | if (!area || !area.type) {
|
370 | return null;
|
371 | }
|
372 | if (area.type === 'tab-area') {
|
373 | return {
|
374 | type: 'tab-area',
|
375 | currentIndex: area.currentIndex,
|
376 | widgets: area.widgets
|
377 | .map(widget => Private.nameProperty.get(widget))
|
378 | .filter(name => !!name)
|
379 | };
|
380 | }
|
381 | return {
|
382 | type: 'split-area',
|
383 | orientation: area.orientation,
|
384 | sizes: area.sizes,
|
385 | children: area.children
|
386 | .map(serializeArea)
|
387 | .filter(area => !!area)
|
388 | };
|
389 | }
|
390 | /**
|
391 | * Return a dehydrated, serializable version of the main dock panel.
|
392 | */
|
393 | function serializeMain(area) {
|
394 | const dehydrated = {
|
395 | dock: (area && area.dock && serializeArea(area.dock.main)) || null
|
396 | };
|
397 | if (area) {
|
398 | if (area.currentWidget) {
|
399 | const current = Private.nameProperty.get(area.currentWidget);
|
400 | if (current) {
|
401 | dehydrated.current = current;
|
402 | }
|
403 | }
|
404 | }
|
405 | return dehydrated;
|
406 | }
|
407 | Private.serializeMain = serializeMain;
|
408 | /**
|
409 | * Deserialize individual areas within the main area.
|
410 | *
|
411 | * #### Notes
|
412 | * Because this data comes from a potentially unreliable foreign source, it is
|
413 | * typed as a `JSONObject`; but the actual expected type is:
|
414 | * `ITabArea | ISplitArea`.
|
415 | *
|
416 | * For fault tolerance, types are manually checked in deserialization.
|
417 | */
|
418 | function deserializeArea(area, names) {
|
419 | if (!area) {
|
420 | return null;
|
421 | }
|
422 | // Because this data is saved to a foreign data source, its type safety is
|
423 | // not guaranteed when it is retrieved, so exhaustive checks are necessary.
|
424 | const type = area.type || 'unknown';
|
425 | if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
|
426 | console.warn(`Attempted to deserialize unknown type: ${type}`);
|
427 | return null;
|
428 | }
|
429 | if (type === 'tab-area') {
|
430 | const { currentIndex, widgets } = area;
|
431 | const hydrated = {
|
432 | type: 'tab-area',
|
433 | currentIndex: currentIndex || 0,
|
434 | widgets: (widgets &&
|
435 | widgets
|
436 | .map(widget => names.get(widget))
|
437 | .filter(widget => !!widget)) ||
|
438 | []
|
439 | };
|
440 | // Make sure the current index is within bounds.
|
441 | if (hydrated.currentIndex > hydrated.widgets.length - 1) {
|
442 | hydrated.currentIndex = 0;
|
443 | }
|
444 | return hydrated;
|
445 | }
|
446 | const { orientation, sizes, children } = area;
|
447 | const hydrated = {
|
448 | type: 'split-area',
|
449 | orientation: orientation,
|
450 | sizes: sizes || [],
|
451 | children: (children &&
|
452 | children
|
453 | .map(child => deserializeArea(child, names))
|
454 | .filter(widget => !!widget)) ||
|
455 | []
|
456 | };
|
457 | return hydrated;
|
458 | }
|
459 | /**
|
460 | * Return the hydrated version of the main dock panel, ready to restore.
|
461 | *
|
462 | * #### Notes
|
463 | * Because this data comes from a potentially unreliable foreign source, it is
|
464 | * typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
|
465 | *
|
466 | * For fault tolerance, types are manually checked in deserialization.
|
467 | */
|
468 | function deserializeMain(area, names) {
|
469 | if (!area) {
|
470 | return null;
|
471 | }
|
472 | const name = area.current || null;
|
473 | const dock = area.dock || null;
|
474 | return {
|
475 | currentWidget: (name && names.has(name) && names.get(name)) || null,
|
476 | dock: dock ? { main: deserializeArea(dock, names) } : null
|
477 | };
|
478 | }
|
479 | Private.deserializeMain = deserializeMain;
|
480 | })(Private || (Private = {}));
|
481 | //# sourceMappingURL=layoutrestorer.js.map |
\ | No newline at end of file |