layoutrestorer.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import {
  6. CommandRegistry
  7. } from '@phosphor/commands';
  8. import {
  9. JSONObject, PromiseDelegate, ReadonlyJSONObject, Token
  10. } from '@phosphor/coreutils';
  11. import {
  12. AttachedProperty
  13. } from '@phosphor/properties';
  14. import {
  15. DockPanel, Widget
  16. } from '@phosphor/widgets';
  17. import {
  18. InstanceTracker
  19. } from '@jupyterlab/apputils';
  20. import {
  21. IStateDB
  22. } from '@jupyterlab/coreutils';
  23. import {
  24. ApplicationShell
  25. } from '.';
  26. /* tslint:disable */
  27. /**
  28. * The layout restorer token.
  29. */
  30. export
  31. const ILayoutRestorer = new Token<ILayoutRestorer>('jupyter.services.layout-restorer');
  32. /* tslint:enable */
  33. /**
  34. * A static class that restores the widgets of the application when it reloads.
  35. */
  36. export
  37. interface ILayoutRestorer {
  38. /**
  39. * A promise resolved when the layout restorer is ready to receive signals.
  40. */
  41. restored: Promise<void>;
  42. /**
  43. * Add a widget to be tracked by the layout restorer.
  44. */
  45. add(widget: Widget, name: string): void;
  46. /**
  47. * Restore the widgets of a particular instance tracker.
  48. *
  49. * @param tracker - The instance tracker whose widgets will be restored.
  50. *
  51. * @param options - The restoration options.
  52. */
  53. restore(tracker: InstanceTracker<any>, options: ILayoutRestorer.IRestoreOptions<any>): void;
  54. }
  55. /**
  56. * A namespace for the layout restorer.
  57. */
  58. export
  59. namespace ILayoutRestorer {
  60. /**
  61. * The state restoration configuration options.
  62. */
  63. export
  64. interface IRestoreOptions<T extends Widget> {
  65. /**
  66. * The command to execute when restoring instances.
  67. */
  68. command: string;
  69. /**
  70. * A function that returns the args needed to restore an instance.
  71. */
  72. args: (widget: T) => ReadonlyJSONObject;
  73. /**
  74. * A function that returns a unique persistent name for this instance.
  75. */
  76. name: (widget: T) => string;
  77. /**
  78. * The point after which it is safe to restore state.
  79. *
  80. * #### Notes
  81. * By definition, this promise or promises will happen after the application
  82. * has `started`.
  83. */
  84. when?: Promise<any> | Array<Promise<any>>;
  85. }
  86. }
  87. /**
  88. * The state database key for restorer data.
  89. */
  90. const KEY = 'layout-restorer:data';
  91. /**
  92. * The default implementation of a layout restorer.
  93. *
  94. * #### Notes
  95. * The lifecycle for state restoration is subtle. The sequence of events is:
  96. *
  97. * 1. The layout restorer plugin is instantiated. It installs itself as the
  98. * layout database that the application shell can use to `fetch` and `save`
  99. * layout restoration data.
  100. *
  101. * 2. Other plugins that care about state restoration require the layout
  102. * restorer as a dependency.
  103. *
  104. * 3. As each load-time plugin initializes (which happens before the lab
  105. * application has `started`), it instructs the layout restorer whether
  106. * the restorer ought to `restore` its state by passing in its tracker.
  107. * Alternatively, a plugin that does not require its own instance tracker
  108. * (because perhaps it only creates a single widget, like a command palette),
  109. * can simply `add` its widget along with a persistent unique name to the
  110. * layout restorer so that its layout state can be restored when the lab
  111. * application restores.
  112. *
  113. * 4. After all the load-time plugins have finished initializing, the lab
  114. * application `started` promise will resolve. This is the `first`
  115. * promise that the layout restorer waits for. By this point, all of the
  116. * plugins that care about restoration will have instructed the layout
  117. * restorer to `restore` their state.
  118. *
  119. * 5. The layout restorer will then instruct each plugin's instance tracker
  120. * to restore its state and reinstantiate whichever widgets it wants. The
  121. * tracker returns a promise to the layout restorer that resolves when it
  122. * has completed restoring the tracked widgets it cares about.
  123. *
  124. * 6. As each instance tracker finishes restoring the widget instances it cares
  125. * about, it resolves the promise that was made to the layout restorer
  126. * (in step 5). After all of the promises that the restorer is awaiting have
  127. * resolved, the restorer then resolves its `restored` promise allowing the
  128. * application shell to `fetch` the dehydrated layout state and rehydrate the
  129. * saved layout.
  130. *
  131. * Of particular note are steps 5 and 6: since state restoration of plugins
  132. * is accomplished by executing commands, the command that is used to restore
  133. * the state of each plugin must return a promise that only resolves when the
  134. * widget has been created and added to the plugin's instance tracker.
  135. */
  136. export
  137. class LayoutRestorer implements ILayoutRestorer {
  138. /**
  139. * Create a layout restorer.
  140. */
  141. constructor(options: LayoutRestorer.IOptions) {
  142. this._registry = options.registry;
  143. this._state = options.state;
  144. this._first = options.first;
  145. this._first.then(() => {
  146. this._firstDone = true;
  147. return Promise.all(this._promises);
  148. }).then(() => {
  149. this._promisesDone = true;
  150. // Release the tracker set.
  151. this._trackers.clear();
  152. }).then(() => {
  153. this._restored.resolve(void 0);
  154. });
  155. }
  156. /**
  157. * A promise resolved when the layout restorer is ready to receive signals.
  158. */
  159. get restored(): Promise<void> {
  160. return this._restored.promise;
  161. }
  162. /**
  163. * Add a widget to be tracked by the layout restorer.
  164. */
  165. add(widget: Widget, name: string): void {
  166. Private.nameProperty.set(widget, name);
  167. this._widgets.set(name, widget);
  168. widget.disposed.connect(this._onWidgetDisposed, this);
  169. }
  170. /**
  171. * Fetch the layout state for the application.
  172. *
  173. * #### Notes
  174. * Fetching the layout relies on all widget restoration to be complete, so
  175. * calls to `fetch` are guaranteed to return after restoration is complete.
  176. */
  177. fetch(): Promise<ApplicationShell.ILayout> {
  178. const blank: ApplicationShell.ILayout = {
  179. fresh: true, mainArea: null, leftArea: null, rightArea: null
  180. };
  181. let layout = this._state.fetch(KEY);
  182. return Promise.all([layout, this.restored]).then(([data]) => {
  183. if (!data) {
  184. return blank;
  185. }
  186. const { main, left, right } = data as Private.ILayout;
  187. // If any data exists, then this is not a fresh session.
  188. const fresh = false;
  189. // Rehydrate main area.
  190. const mainArea = this._rehydrateMainArea(main);
  191. // Rehydrate left area.
  192. const leftArea = this._rehydrateSideArea(left);
  193. // Rehydrate right area.
  194. const rightArea = this._rehydrateSideArea(right);
  195. return { fresh, mainArea, leftArea, rightArea };
  196. }).catch(() => blank); // Let fetch fail gracefully; return blank slate.
  197. }
  198. /**
  199. * Restore the widgets of a particular instance tracker.
  200. *
  201. * @param tracker - The instance tracker whose widgets will be restored.
  202. *
  203. * @param options - The restoration options.
  204. */
  205. restore(tracker: InstanceTracker<Widget>, options: ILayoutRestorer.IRestoreOptions<Widget>): Promise<any> {
  206. if (this._firstDone) {
  207. const warning = 'restore() can only be called before `first` has resolved.';
  208. console.warn(warning);
  209. return Promise.reject(warning);
  210. }
  211. const { namespace } = tracker;
  212. if (this._trackers.has(namespace)) {
  213. let warning = `A tracker namespaced ${namespace} was already restored.`;
  214. console.warn(warning);
  215. return Promise.reject(warning);
  216. }
  217. const { args, command, name, when } = options;
  218. // Add the tracker to the private trackers collection.
  219. this._trackers.add(namespace);
  220. // Whenever a new widget is added to the tracker, record its name.
  221. tracker.widgetAdded.connect((sender: any, widget: Widget) => {
  222. const widgetName = name(widget);
  223. if (widgetName) {
  224. this.add(widget, `${namespace}:${widgetName}`);
  225. }
  226. }, this);
  227. // Whenever a widget is updated, get its new name.
  228. tracker.widgetUpdated.connect((sender, widget) => {
  229. const widgetName = name(widget);
  230. if (widgetName) {
  231. let name = `${namespace}:${widgetName}`;
  232. Private.nameProperty.set(widget, name);
  233. this._widgets.set(name, widget);
  234. }
  235. });
  236. const first = this._first;
  237. const promise = tracker.restore({
  238. args, command, name,
  239. registry: this._registry,
  240. state: this._state,
  241. when: when ? [first].concat(when) : first
  242. });
  243. this._promises.push(promise);
  244. return promise;
  245. }
  246. /**
  247. * Save the layout state for the application.
  248. */
  249. save(data: ApplicationShell.ILayout): Promise<void> {
  250. // If there are promises that are unresolved, bail.
  251. if (!this._promisesDone) {
  252. let warning = 'save() was called prematurely.';
  253. console.warn(warning);
  254. return Promise.reject(warning);
  255. }
  256. let dehydrated: Private.ILayout = {};
  257. // Dehydrate main area.
  258. dehydrated.main = this._dehydrateMainArea(data.mainArea);
  259. // Dehydrate left area.
  260. dehydrated.left = this._dehydrateSideArea(data.leftArea);
  261. // Dehydrate right area.
  262. dehydrated.right = this._dehydrateSideArea(data.rightArea);
  263. return this._state.save(KEY, dehydrated);
  264. }
  265. /**
  266. * Dehydrate a main area description into a serializable object.
  267. */
  268. private _dehydrateMainArea(area: ApplicationShell.IMainArea | null): Private.IMainArea | null {
  269. if (!area) {
  270. return null;
  271. }
  272. return Private.serializeMain(area);
  273. }
  274. /**
  275. * Reydrate a serialized main area description object.
  276. *
  277. * #### Notes
  278. * This function consumes data that can become corrupted, so it uses type
  279. * coercion to guarantee the dehydrated object is safely processed.
  280. */
  281. private _rehydrateMainArea(area?: Private.IMainArea | null): ApplicationShell.IMainArea | null {
  282. if (!area) {
  283. return null;
  284. }
  285. return Private.deserializeMain(area, this._widgets);
  286. }
  287. /**
  288. * Dehydrate a side area description into a serializable object.
  289. */
  290. private _dehydrateSideArea(area?: ApplicationShell.ISideArea | null): Private.ISideArea | null {
  291. if (!area) {
  292. return null;
  293. }
  294. let dehydrated: Private.ISideArea = { collapsed: area.collapsed };
  295. if (area.currentWidget) {
  296. let current = Private.nameProperty.get(area.currentWidget);
  297. if (current) {
  298. dehydrated.current = current;
  299. }
  300. }
  301. if (area.widgets) {
  302. dehydrated.widgets = area.widgets
  303. .map(widget => Private.nameProperty.get(widget))
  304. .filter(name => !!name);
  305. }
  306. return dehydrated;
  307. }
  308. /**
  309. * Reydrate a serialized side area description object.
  310. *
  311. * #### Notes
  312. * This function consumes data that can become corrupted, so it uses type
  313. * coercion to guarantee the dehydrated object is safely processed.
  314. */
  315. private _rehydrateSideArea(area?: Private.ISideArea | null): ApplicationShell.ISideArea {
  316. if (!area) {
  317. return { collapsed: true, currentWidget: null, widgets: null };
  318. }
  319. let internal = this._widgets;
  320. const collapsed = area.hasOwnProperty('collapsed') ? !!area.collapsed
  321. : false;
  322. const currentWidget = area.current && internal.has(`${area.current}`) ?
  323. internal.get(`${area.current}`) : null;
  324. const widgets = !Array.isArray(area.widgets) ? null
  325. : area.widgets
  326. .map(name => internal.has(`${name}`) ? internal.get(`${name}`) : null)
  327. .filter(widget => !!widget);
  328. return {
  329. collapsed,
  330. currentWidget: currentWidget!,
  331. widgets: widgets as Widget[] | null
  332. };
  333. }
  334. /**
  335. * Handle a widget disposal.
  336. */
  337. private _onWidgetDisposed(widget: Widget): void {
  338. let name = Private.nameProperty.get(widget);
  339. this._widgets.delete(name);
  340. }
  341. private _first: Promise<any>;
  342. private _firstDone = false;
  343. private _promisesDone = false;
  344. private _promises: Promise<any>[] = [];
  345. private _restored = new PromiseDelegate<void>();
  346. private _registry: CommandRegistry ;
  347. private _state: IStateDB;
  348. private _trackers = new Set<string>();
  349. private _widgets = new Map<string, Widget>();
  350. }
  351. /**
  352. * A namespace for `LayoutRestorer` statics.
  353. */
  354. export
  355. namespace LayoutRestorer {
  356. /**
  357. * The configuration options for layout restorer instantiation.
  358. */
  359. export
  360. interface IOptions {
  361. /**
  362. * The initial promise that has to be resolved before restoration.
  363. *
  364. * #### Notes
  365. * This promise should equal the JupyterLab application `started` notifier.
  366. */
  367. first: Promise<any>;
  368. /**
  369. * The application command registry.
  370. */
  371. registry: CommandRegistry;
  372. /**
  373. * The state database instance.
  374. */
  375. state: IStateDB;
  376. }
  377. }
  378. /*
  379. * A namespace for private data.
  380. */
  381. namespace Private {
  382. /**
  383. * The dehydrated state of the application layout.
  384. *
  385. * #### Notes
  386. * This format is JSON serializable and saved in the state database.
  387. * It is meant to be a data structure can translate into an
  388. * `ApplicationShell.ILayout` data structure for consumption by
  389. * the application shell.
  390. */
  391. export
  392. interface ILayout extends JSONObject {
  393. /**
  394. * The main area of the user interface.
  395. */
  396. main?: IMainArea | null;
  397. /**
  398. * The left area of the user interface.
  399. */
  400. left?: ISideArea | null;
  401. /**
  402. * The right area of the user interface.
  403. */
  404. right?: ISideArea | null;
  405. }
  406. /**
  407. * The restorable description of the main application area.
  408. */
  409. export
  410. interface IMainArea extends JSONObject {
  411. /**
  412. * The current widget that has application focus.
  413. */
  414. current?: string | null;
  415. /**
  416. * The main application dock panel.
  417. */
  418. dock?: ISplitArea | ITabArea | null;
  419. /**
  420. * The document mode (i.e., multiple/single) of the main dock panel.
  421. */
  422. mode?: DockPanel.Mode | null;
  423. }
  424. /**
  425. * The restorable description of a sidebar in the user interface.
  426. */
  427. export
  428. interface ISideArea extends JSONObject {
  429. /**
  430. * A flag denoting whether the sidebar has been collapsed.
  431. */
  432. collapsed?: boolean | null;
  433. /**
  434. * The current widget that has side area focus.
  435. */
  436. current?: string | null;
  437. /**
  438. * The collection of widgets held by the sidebar.
  439. */
  440. widgets?: Array<string> | null;
  441. }
  442. /**
  443. * The restorable description of a tab area in the user interface.
  444. */
  445. export
  446. interface ITabArea extends JSONObject {
  447. /**
  448. * The type indicator of the serialized tab area.
  449. */
  450. type: 'tab-area';
  451. /**
  452. * The widgets in the tab area.
  453. */
  454. widgets: Array<string> | null;
  455. /**
  456. * The index of the selected tab.
  457. */
  458. currentIndex: number;
  459. }
  460. /**
  461. * The restorable description of a split area in the user interface.
  462. */
  463. export
  464. interface ISplitArea extends JSONObject {
  465. /**
  466. * The type indicator of the serialized split area.
  467. */
  468. type: 'split-area';
  469. /**
  470. * The orientation of the split area.
  471. */
  472. orientation: 'horizontal' | 'vertical';
  473. /**
  474. * The children in the split area.
  475. */
  476. children: Array<ITabArea | ISplitArea> | null;
  477. /**
  478. * The sizes of the children.
  479. */
  480. sizes: Array<number>;
  481. }
  482. /**
  483. * An attached property for a widget's ID in the state database.
  484. */
  485. export
  486. const nameProperty = new AttachedProperty<Widget, string>({
  487. name: 'name',
  488. create: owner => ''
  489. });
  490. /**
  491. * Serialize individual areas within the main area.
  492. */
  493. function serializeArea(area: ApplicationShell.AreaConfig | null): ITabArea | ISplitArea | null {
  494. if (!area || !area.type) {
  495. return null;
  496. }
  497. if (area.type === 'tab-area') {
  498. return {
  499. type: 'tab-area',
  500. currentIndex: area.currentIndex,
  501. widgets: area.widgets
  502. .map(widget => nameProperty.get(widget))
  503. .filter(name => !!name)
  504. };
  505. }
  506. return {
  507. type: 'split-area',
  508. orientation: area.orientation,
  509. sizes: area.sizes,
  510. children: area.children.map(serializeArea)
  511. .filter(area => !!area) as (ITabArea | ISplitArea)[]
  512. };
  513. }
  514. /**
  515. * Return a dehydrated, serializable version of the main dock panel.
  516. */
  517. export
  518. function serializeMain(area: ApplicationShell.IMainArea): IMainArea {
  519. let dehydrated: IMainArea = {
  520. dock: area && area.dock && serializeArea(area.dock.main) || null
  521. };
  522. if (area) {
  523. dehydrated.mode = area.mode;
  524. if (area.currentWidget) {
  525. let current = Private.nameProperty.get(area.currentWidget);
  526. if (current) {
  527. dehydrated.current = current;
  528. }
  529. }
  530. }
  531. return dehydrated;
  532. }
  533. /**
  534. * Deserialize individual areas within the main area.
  535. *
  536. * #### Notes
  537. * Because this data comes from a potentially unreliable foreign source, it is
  538. * typed as a `JSONObject`; but the actual expected type is:
  539. * `ITabArea | ISplitArea`.
  540. *
  541. * For fault tolerance, types are manually checked in deserialization.
  542. */
  543. function deserializeArea(area: JSONObject, names: Map<string, Widget>): ApplicationShell.AreaConfig | null {
  544. if (!area) {
  545. return null;
  546. }
  547. // Because this data is saved to a foreign data source, its type safety is
  548. // not guaranteed when it is retrieved, so exhaustive checks are necessary.
  549. const type = (area as any).type as string || 'unknown';
  550. if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
  551. console.warn(`Attempted to deserialize unknown type: ${type}`);
  552. return null;
  553. }
  554. if (type === 'tab-area') {
  555. const { currentIndex, widgets } = area as ITabArea;
  556. let hydrated: ApplicationShell.AreaConfig = {
  557. type: 'tab-area',
  558. currentIndex: currentIndex || 0,
  559. widgets: widgets && widgets.map(widget => names.get(widget))
  560. .filter(widget => !!widget) as Widget[] || []
  561. };
  562. // Make sure the current index is within bounds.
  563. if (hydrated.currentIndex > hydrated.widgets.length - 1) {
  564. hydrated.currentIndex = 0;
  565. }
  566. return hydrated;
  567. }
  568. const { orientation, sizes, children } = area as ISplitArea;
  569. let hydrated: ApplicationShell.AreaConfig = {
  570. type: 'split-area',
  571. orientation: orientation,
  572. sizes: sizes || [],
  573. children: children &&
  574. children.map(child => deserializeArea(child, names))
  575. .filter(widget => !!widget) as ApplicationShell.AreaConfig[] || []
  576. };
  577. return hydrated;
  578. }
  579. /**
  580. * Return the hydrated version of the main dock panel, ready to restore.
  581. *
  582. * #### Notes
  583. * Because this data comes from a potentially unreliable foreign source, it is
  584. * typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
  585. *
  586. * For fault tolerance, types are manually checked in deserialization.
  587. */
  588. export
  589. function deserializeMain(area: JSONObject, names: Map<string, Widget>): ApplicationShell.IMainArea | null {
  590. if (!area) {
  591. return null;
  592. }
  593. const name = (area as any).current || null;
  594. const dock = (area as any).dock || null;
  595. const mode = (area as any).mode || null;
  596. return {
  597. currentWidget: name && names.has(name) && names.get(name) || null,
  598. dock: dock ? { main: deserializeArea(dock, names) } : null,
  599. mode: mode === 'multiple-document' || mode === 'single-document' ? mode
  600. : null
  601. };
  602. }
  603. }