UNPKG

17.3 kBJavaScriptView Raw
1/* -----------------------------------------------------------------------------
2| Copyright (c) Jupyter Development Team.
3| Distributed under the terms of the Modified BSD License.
4|----------------------------------------------------------------------------*/
5import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils';
6import { AttachedProperty } from '@lumino/properties';
7/**
8 * The layout restorer token.
9 */
10export const ILayoutRestorer = new Token('@jupyterlab/application:ILayoutRestorer');
11/**
12 * The data connector key for restorer data.
13 */
14const 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 */
64export 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 */
356var 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