layoutrestorer.ts 18 KB

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