index.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. 'use strict';
  4. import {
  5. IContentsModel, IKernelId, IKernelSpecId, IContentsOpts, IKernel,
  6. INotebookSession, IContentsManager, INotebookSessionManager,
  7. IKernelSpecIds, ISessionId
  8. } from 'jupyter-js-services';
  9. import {
  10. IDisposable, DisposableDelegate
  11. } from 'phosphor-disposable';
  12. import {
  13. IMessageFilter, IMessageHandler, Message, installMessageFilter
  14. } from 'phosphor-messaging';
  15. import {
  16. PanelLayout
  17. } from 'phosphor-panel';
  18. import {
  19. Property
  20. } from 'phosphor-properties';
  21. import {
  22. ISignal, Signal
  23. } from 'phosphor-signaling';
  24. import {
  25. Widget
  26. } from 'phosphor-widget';
  27. import {
  28. showDialog
  29. } from '../dialog';
  30. import {
  31. IWidgetOpener
  32. } from '../filebrowser/browser';
  33. import {
  34. ContextManager
  35. } from './context';
  36. /**
  37. * The interface for a document model.
  38. */
  39. export
  40. interface IDocumentModel extends IDisposable {
  41. /**
  42. * A signal emitted when the document content changes.
  43. */
  44. contentChanged: ISignal<IDocumentModel, any>;
  45. /**
  46. * A signal emitted when the model dirty state changes.
  47. */
  48. dirtyChanged: ISignal<IDocumentModel, boolean>;
  49. /**
  50. * Serialize the model to a string.
  51. */
  52. toString(): string;
  53. /**
  54. * Deserialize the model from a string.
  55. *
  56. * #### Notes
  57. * Should emit a [contentChanged] signal.
  58. */
  59. fromString(value: string): void;
  60. /**
  61. * Serialize the model to JSON.
  62. */
  63. toJSON(): any;
  64. /**
  65. * Deserialize the model from JSON.
  66. *
  67. * #### Notes
  68. * Should emit a [contentChanged] signal.
  69. */
  70. fromJSON(value: any): void;
  71. /**
  72. * The dirty state of the model.
  73. *
  74. * #### Notes
  75. * This should be cleared when the document is loaded from
  76. * or saved to disk.
  77. */
  78. dirty: boolean;
  79. /**
  80. * The read-only state of the model.
  81. */
  82. readOnly: boolean;
  83. /**
  84. * The default kernel name of the document.
  85. *
  86. * #### Notes
  87. * This is a read-only property.
  88. */
  89. defaultKernelName: string;
  90. /**
  91. * The default kernel language of the document.
  92. *
  93. * #### Notes
  94. * This is a read-only property.
  95. */
  96. defaultKernelLanguage: string;
  97. }
  98. /**
  99. * The document context object.
  100. */
  101. export interface IDocumentContext extends IDisposable {
  102. /**
  103. * The unique id of the context.
  104. *
  105. * #### Notes
  106. * This is a read-only property.
  107. */
  108. id: string;
  109. /**
  110. * The current kernel associated with the document.
  111. *
  112. * #### Notes
  113. * This is a read-only propery.
  114. */
  115. kernel: IKernel;
  116. /**
  117. * The current path associated with the document.
  118. *
  119. * #### Notes
  120. * This is a read-only property.
  121. */
  122. path: string;
  123. /**
  124. * The current contents model associated with the document
  125. *
  126. * #### Notes
  127. * This is a read-only property. The model will have an
  128. * empty `contents` field.
  129. */
  130. contentsModel: IContentsModel;
  131. /**
  132. * Get the kernel spec information.
  133. *
  134. * #### Notes
  135. * This is a read-only property.
  136. */
  137. kernelSpecs: IKernelSpecIds;
  138. /**
  139. * A signal emitted when the kernel changes.
  140. */
  141. kernelChanged: ISignal<IDocumentContext, IKernel>;
  142. /**
  143. * A signal emitted when the path changes.
  144. */
  145. pathChanged: ISignal<IDocumentContext, string>;
  146. /**
  147. * Change the current kernel associated with the document.
  148. */
  149. changeKernel(options: IKernelId): Promise<IKernel>;
  150. /**
  151. * Save the document contents to disk.
  152. */
  153. save(): Promise<void>;
  154. /**
  155. * Revert the document contents to disk contents.
  156. */
  157. revert(): Promise<void>;
  158. /**
  159. * Get the list of running sessions.
  160. */
  161. listSessions(): Promise<ISessionId[]>;
  162. /**
  163. * Add a sibling widget to the document manager.
  164. *
  165. * @param widget - The widget to add to the document manager.
  166. *
  167. * @returns A disposable used to remove the sibling if desired.
  168. *
  169. * #### Notes
  170. * It is assumed that the widget has the same model and context
  171. * as the original widget.
  172. */
  173. addSibling(widget: Widget): IDisposable;
  174. }
  175. /**
  176. * The options used to register a widget factory.
  177. */
  178. export
  179. interface IWidgetFactoryOptions {
  180. /**
  181. * The file extensions the widget can view.
  182. *
  183. * #### Notes
  184. * Use `'.*'` to denote all file extensions
  185. * or give the actual extension (e.g. `'.txt'`).
  186. */
  187. fileExtensions: string[];
  188. /**
  189. * The name of the widget to display in dialogs.
  190. */
  191. displayName: string;
  192. /**
  193. * The registered name of the model type used to create the widgets.
  194. */
  195. modelName: string;
  196. /**
  197. * The file extensions for which the factory should be the default.
  198. * #### Notes
  199. * Use `'.*'` to denote all file extensions
  200. * or give the actual extension (e.g. `'.txt'`).
  201. */
  202. defaultFor?: string[];
  203. /**
  204. * Whether the widgets prefer having a kernel started.
  205. */
  206. preferKernel?: boolean;
  207. /**
  208. * Whether the widgets can start a kernel when opened.
  209. */
  210. canStartKernel?: boolean;
  211. }
  212. /**
  213. * The interface for a widget factory.
  214. */
  215. export
  216. interface IWidgetFactory<T extends Widget> extends IDisposable {
  217. /**
  218. * Create a new widget.
  219. */
  220. createNew(model: IDocumentModel, context: IDocumentContext, kernel: IKernelId): T;
  221. /**
  222. * Take an action on a widget before closing it.
  223. *
  224. * @returns A promise that resolves to true if the document should close
  225. * and false otherwise.
  226. */
  227. beforeClose(model: IDocumentModel, context: IDocumentContext, widget: Widget): Promise<boolean>;
  228. }
  229. /**
  230. * The options used to register a model factory.
  231. */
  232. export
  233. interface IModelFactoryOptions {
  234. /**
  235. * The name of the model factory.
  236. */
  237. name: string;
  238. /**
  239. * The contents options used to fetch/save files.
  240. */
  241. contentsOptions: IContentsOpts;
  242. }
  243. /**
  244. * The interface for a model factory.
  245. */
  246. export
  247. interface IModelFactory extends IDisposable {
  248. /**
  249. * Create a new model for a given path.
  250. *
  251. * @param languagePreference - An optional kernel language preference.
  252. *
  253. * @returns A new document model.
  254. */
  255. createNew(languagePreference?: string): IDocumentModel;
  256. /**
  257. * Get the preferred kernel language given a path.
  258. */
  259. preferredLanguage(path: string): string;
  260. }
  261. /**
  262. * A kernel preference for a given file path and widget.
  263. */
  264. export
  265. interface IKernelPreference {
  266. /**
  267. * The preferred kernel language.
  268. */
  269. language: string;
  270. /**
  271. * Whether to prefer having a kernel started when opening.
  272. */
  273. preferKernel: boolean;
  274. /**
  275. * Whether a kernel when can be started when opening.
  276. */
  277. canStartKernel: boolean;
  278. }
  279. /**
  280. * The document manager.
  281. *
  282. * #### Notes
  283. * The document manager is used to register model and widget creators,
  284. * and the file browser uses the document manager to create widgets. The
  285. * document manager maintains a context for each path and model type that is
  286. * open, and a list of widgets for each context. The document manager is in
  287. * control of the proper closing and disposal of the widgets and contexts.
  288. */
  289. export
  290. class DocumentManager implements IDisposable {
  291. /**
  292. * Construct a new document manager.
  293. */
  294. constructor(contentsManager: IContentsManager, sessionManager: INotebookSessionManager, kernelSpecs: IKernelSpecIds, opener: IWidgetOpener) {
  295. this._contentsManager = contentsManager;
  296. this._sessionManager = sessionManager;
  297. this._contextManager = new ContextManager(contentsManager, sessionManager, kernelSpecs, (id: string, widget: Widget) => {
  298. let parent = new Widget();
  299. this._attachChild(parent, widget);
  300. Private.contextProperty.set(parent, id);
  301. this._widgets[id].push(parent);
  302. opener.open(parent);
  303. return new DisposableDelegate(() => {
  304. parent.close();
  305. });
  306. });
  307. }
  308. /**
  309. * Get whether the document manager has been disposed.
  310. */
  311. get isDisposed(): boolean {
  312. return this._contentsManager === null;
  313. }
  314. /**
  315. * Dispose of the resources held by the document manager.
  316. */
  317. dispose(): void {
  318. if (this.isDisposed) {
  319. return;
  320. }
  321. for (let modelName in this._modelFactories) {
  322. this._modelFactories[modelName].factory.dispose();
  323. }
  324. this._modelFactories = null;
  325. for (let widgetName in this._widgetFactories) {
  326. this._widgetFactories[widgetName].factory.dispose();
  327. }
  328. this._widgetFactories = null;
  329. for (let id in this._widgets) {
  330. for (let widget of this._widgets[id]) {
  331. widget.dispose();
  332. }
  333. }
  334. this._widgets = null;
  335. this._contentsManager = null;
  336. this._sessionManager = null;
  337. this._contextManager.dispose();
  338. this._contextManager = null;
  339. }
  340. /**
  341. * Register a widget factory with the document manager.
  342. *
  343. * @param factory - The factory instance.
  344. *
  345. * @param options - The options used to register the factory.
  346. *
  347. * @returns A disposable used to unregister the factory.
  348. *
  349. * #### Notes
  350. * If a factory with the given `displayName` is already registered,
  351. * an error will be thrown.
  352. * If `'.*'` is given as a default extension, the factory will be registered
  353. * as the global default.
  354. * If a factory is already registered as a default for a given extension or
  355. * as the global default, this factory will override the existing default.
  356. */
  357. registerWidgetFactory(factory: IWidgetFactory<Widget>, options: IWidgetFactoryOptions): IDisposable {
  358. let name = options.displayName;
  359. let exOpt = options as Private.IWidgetFactoryEx;
  360. exOpt.factory = factory;
  361. if (this._widgetFactories[name]) {
  362. throw new Error(`Duplicate registered factory ${name}`);
  363. }
  364. this._widgetFactories[name] = exOpt;
  365. if (options.defaultFor) {
  366. for (let option of options.defaultFor) {
  367. if (option === '.*') {
  368. this._defaultWidgetFactory = name;
  369. } else if (option in options.fileExtensions) {
  370. this._defaultWidgetFactories[option] = name;
  371. }
  372. }
  373. }
  374. return new DisposableDelegate(() => {
  375. delete this._widgetFactories[name];
  376. if (this._defaultWidgetFactory === name) {
  377. this._defaultWidgetFactory = '';
  378. }
  379. for (let opt in Object.keys(this._defaultWidgetFactories)) {
  380. let n = this._defaultWidgetFactories[opt];
  381. if (n === name) {
  382. delete this._defaultWidgetFactories[opt];
  383. }
  384. }
  385. });
  386. }
  387. /**
  388. * Register a model factory.
  389. *
  390. * @param factory - The factory instance.
  391. *
  392. * @param options - The options used to register the factory.
  393. *
  394. * @returns A disposable used to unregister the factory.
  395. *
  396. * #### Notes
  397. * If a factory with the given `name` is already registered, an error
  398. * will be thrown.
  399. */
  400. registerModelFactory(factory: IModelFactory, options: IModelFactoryOptions): IDisposable {
  401. let exOpt = options as Private.IModelFactoryEx;
  402. let name = options.name;
  403. exOpt.factory = factory;
  404. if (this._modelFactories[name]) {
  405. throw new Error(`Duplicate registered factory ${name}`);
  406. }
  407. this._modelFactories[name] = exOpt;
  408. return new DisposableDelegate(() => {
  409. delete this._modelFactories[name];
  410. });
  411. }
  412. /**
  413. * Get the list of registered widget factory display names.
  414. *
  415. * @param path - An optional file path to filter the results.
  416. *
  417. * #### Notes
  418. * The first item in the list is considered the default.
  419. */
  420. listWidgetFactories(path?: string): string[] {
  421. let ext = '.' + path.split('.').pop();
  422. let factories: string[] = [];
  423. let options: Private.IWidgetFactoryEx;
  424. let name = '';
  425. // If an extension was given, filter by extension.
  426. // Make sure the modelFactory is registered.
  427. if (ext.length > 1) {
  428. if (ext in this._defaultWidgetFactories) {
  429. name = this._defaultWidgetFactories[ext];
  430. options = this._widgetFactories[name];
  431. if (options.modelName in this._modelFactories) {
  432. factories.push(name);
  433. }
  434. }
  435. }
  436. // Add the default widget if it was not already added.
  437. if (name !== this._defaultWidgetFactory && this._defaultWidgetFactory) {
  438. name = this._defaultWidgetFactory;
  439. options = this._widgetFactories[name];
  440. if (options.modelName in this._modelFactories) {
  441. factories.push(name);
  442. }
  443. }
  444. // Add the rest of the valid widgetFactories that can open the path.
  445. for (name in this._widgetFactories) {
  446. if (factories.indexOf(name) !== -1) {
  447. continue;
  448. }
  449. options = this._widgetFactories[name];
  450. if (!(options.modelName in this._modelFactories)) {
  451. continue;
  452. }
  453. let exts = options.fileExtensions;
  454. if ((ext in exts) || ('.*' in exts)) {
  455. factories.push(name);
  456. }
  457. }
  458. return factories;
  459. }
  460. /**
  461. * Get the kernel preference.
  462. */
  463. getKernelPreference(path: string, widgetName: string): IKernelPreference {
  464. let widgetFactoryEx = this._getWidgetFactoryEx(widgetName);
  465. let modelFactoryEx = this._getModelFactoryEx(widgetName);
  466. let language = modelFactoryEx.factory.preferredLanguage(path);
  467. return {
  468. language,
  469. preferKernel: widgetFactoryEx.preferKernel,
  470. canStartKernel: widgetFactoryEx.canStartKernel
  471. }
  472. }
  473. /**
  474. * Open a file and return the widget used to display the contents.
  475. *
  476. * @param path - The file path to open.
  477. *
  478. * @param widgetName - The name of the widget factory to use.
  479. *
  480. * @param kernel - An optional kernel name/id to override the default.
  481. */
  482. open(path: string, widgetName='default', kernel?: IKernelId): Widget {
  483. let widget = new Widget();
  484. let manager = this._contentsManager;
  485. let mFactoryEx: Private.IModelFactoryEx;
  486. if (widgetName !== 'default') {
  487. mFactoryEx = this._getModelFactoryEx(widgetName);
  488. } else {
  489. widgetName = this.listWidgetFactories(path)[0];
  490. mFactoryEx = this._getModelFactoryEx(widgetName);
  491. }
  492. let lang = mFactoryEx.factory.preferredLanguage(path);
  493. let model: IDocumentModel;
  494. let id = this._contextManager.findContext(path, mFactoryEx.name);
  495. if (id) {
  496. model = this._contextManager.getModel(id);
  497. } else {
  498. model = mFactoryEx.factory.createNew(lang);
  499. }
  500. let opts = mFactoryEx.contentsOptions;
  501. manager.get(path, opts).then(contents => {
  502. if (contents.format === 'json') {
  503. model.fromJSON(contents.content);
  504. } else {
  505. model.fromString(contents.content);
  506. }
  507. model.dirty = false;
  508. id = this._createContext(path, model, widgetName, contents);
  509. this._createWidget(id, widgetName, widget, kernel);
  510. });
  511. installMessageFilter(widget, this);
  512. return widget;
  513. }
  514. /**
  515. * Create a new file of the given name.
  516. *
  517. * @param path - The file path to use.
  518. *
  519. * @param widgetName - The name of the widget factory to use.
  520. *
  521. * @param kernel - An optional kernel name/id to override the default.
  522. */
  523. createNew(path: string, widgetName='default', kernel?: IKernelId): Widget {
  524. let widget = new Widget();
  525. let manager = this._contentsManager;
  526. let mFactoryEx: Private.IModelFactoryEx;
  527. if (widgetName !== 'default') {
  528. mFactoryEx = this._getModelFactoryEx(widgetName);
  529. } else {
  530. widgetName = this.listWidgetFactories(path)[0];
  531. mFactoryEx = this._getModelFactoryEx(widgetName);
  532. }
  533. if (!mFactoryEx) {
  534. return;
  535. }
  536. let lang = mFactoryEx.factory.preferredLanguage(path);
  537. let model = mFactoryEx.factory.createNew(lang);
  538. let opts = mFactoryEx.contentsOptions;
  539. if (opts.format === 'json') {
  540. opts.content = model.toJSON();
  541. } else {
  542. opts.content = model.toString();
  543. }
  544. manager.save(path, opts).then(contents => {
  545. let id = this._createContext(path, model, widgetName, contents);
  546. this._createWidget(id, widgetName, widget, kernel);
  547. });
  548. installMessageFilter(widget, this);
  549. return widget;
  550. }
  551. /**
  552. * Get the path given a widget.
  553. */
  554. getPath(widget: Widget): string {
  555. let id = Private.contextProperty.get(widget);
  556. return this._contextManager.getPath(id);
  557. }
  558. /**
  559. * Filter messages on the widget.
  560. */
  561. filterMessage(handler: IMessageHandler, msg: Message): boolean {
  562. if (msg.type !== 'close-request') {
  563. return false;
  564. }
  565. if (this._closeGuard) {
  566. // Allow the close to propagate to the widget and its layout.
  567. this._closeGuard = false;
  568. return false;
  569. }
  570. let widget = handler as Widget;
  571. let id = Private.contextProperty.get(widget);
  572. let model = this._contextManager.getModel(id);
  573. let context = this._contextManager.getContext(id);
  574. let child = (widget.layout as PanelLayout).childAt(0);
  575. let name = Private.nameProperty.get(widget);
  576. // Check for a sibling widget.
  577. if (!name) {
  578. this._closeGuard = true;
  579. widget.close();
  580. }
  581. let factory = this._widgetFactories[name].factory;
  582. this._maybeClose(widget, model.dirty).then(result => {
  583. if (!result) {
  584. return result;
  585. }
  586. return factory.beforeClose(model, context, child);
  587. }).then(result => {
  588. if (result) {
  589. return this._cleanupWidget(widget);
  590. }
  591. });
  592. return true;
  593. }
  594. /**
  595. * Update the path of an open document.
  596. *
  597. * @param oldPath - The previous path.
  598. *
  599. * @param newPath - The new path.
  600. */
  601. renameFile(oldPath: string, newPath: string): void {
  602. let ids = this._contextManager.getIdsForPath(oldPath);
  603. for (let id in ids) {
  604. this._contextManager.rename(id, newPath);
  605. }
  606. }
  607. /**
  608. * Handle a file deletion on the currently open widgets.
  609. *
  610. * @param path - The path of the file to delete.
  611. */
  612. deleteFile(path: string): void {
  613. let ids = this._contextManager.getIdsForPath(path);
  614. for (let id of ids) {
  615. let widgets: Widget[] = this._widgets[id] || [];
  616. for (let w of widgets) {
  617. this._cleanupWidget(w);
  618. }
  619. }
  620. }
  621. /**
  622. * See if a widget already exists for the given path and widget name.
  623. *
  624. * #### Notes
  625. * This can be used to use an existing widget instead of opening
  626. * a new widget.
  627. */
  628. findWidget(path: string, widgetName='default'): Widget {
  629. let ids = this._contextManager.getIdsForPath(path);
  630. if (widgetName === 'default') {
  631. widgetName = this._defaultWidgetFactory;
  632. }
  633. for (let id of ids) {
  634. for (let widget of this._widgets[id]) {
  635. if (Private.nameProperty.get(widget) === widgetName) {
  636. return widget;
  637. }
  638. }
  639. }
  640. }
  641. /**
  642. * Save the document contents to disk.
  643. *
  644. * #### Notes
  645. * This will affect the contents of all other widgets
  646. * that share the same model as the given widget.
  647. */
  648. save(widget: Widget): Promise<void> {
  649. let id = Private.contextProperty.get(widget);
  650. return this._contextManager.save(id);
  651. }
  652. /**
  653. * Save a widget to a different file name.
  654. *
  655. * #### Notes
  656. * It is assumed that all other widgets associated with the new path
  657. * have been closed and that the path is either not in conflict
  658. * or the user has chosen to overwrite the file.
  659. * This will affect the contents of all other widgets
  660. * that share the same model as the given widget.
  661. */
  662. saveAs(widget: Widget, path: string): Promise<void> {
  663. let id = Private.contextProperty.get(widget);
  664. this._contextManager.rename(id, path);
  665. return this._contextManager.save(id);
  666. }
  667. /**
  668. * Revert the document contents to disk contents.
  669. *
  670. * #### Notes
  671. * This will affect the contents of all other widgets
  672. * that share the same model as the given widget.
  673. */
  674. revert(widget: Widget): Promise<void> {
  675. let id = Private.contextProperty.get(widget);
  676. return this._contextManager.revert(id);
  677. }
  678. /**
  679. * Close the widgets associated with a given path.
  680. */
  681. closeFile(path: string): void {
  682. let ids = this._contextManager.getIdsForPath(path);
  683. for (let id of ids) {
  684. let widgets: Widget[] = this._widgets[id] || [];
  685. for (let w of widgets) {
  686. w.close();
  687. }
  688. }
  689. }
  690. /**
  691. * Close all of the open documents.
  692. */
  693. closeAll(): void {
  694. for (let id in this._widgets) {
  695. for (let w of this._widgets[id]) {
  696. w.close();
  697. }
  698. }
  699. }
  700. /**
  701. * Create a context or reuse an existing one.
  702. */
  703. private _createContext(path: string, model: IDocumentModel, widgetName: string, contents: IContentsModel): string {
  704. let mFactoryEx = this._getModelFactoryEx(widgetName);
  705. let id = this._contextManager.findContext(path, mFactoryEx.name);
  706. if (id) {
  707. return id;
  708. } else {
  709. return this._contextManager.createNew(path, model, mFactoryEx, contents);
  710. }
  711. }
  712. /**
  713. * Create a widget from a context and attach it to the parent.
  714. */
  715. private _createWidget(contextId: string, widgetName: string, parent: Widget, kernel?: IKernelId): void {
  716. let wFactoryEx = this._getWidgetFactoryEx(widgetName);
  717. if (!(contextId in this._widgets)) {
  718. this._widgets[contextId] = [];
  719. }
  720. this._widgets[contextId].push(parent);
  721. let context = this._contextManager.getContext(contextId);
  722. let model = this._contextManager.getModel(contextId);
  723. // Create the child widget using the factory.
  724. let child = wFactoryEx.factory.createNew(model, context, kernel);
  725. this._attachChild(parent, child);
  726. Private.nameProperty.set(parent, widgetName);
  727. Private.contextProperty.set(parent, contextId);
  728. }
  729. /**
  730. * Attach a child widget to a parent container.
  731. */
  732. private _attachChild(parent: Widget, child: Widget) {
  733. parent.layout = new PanelLayout();
  734. parent.title.closable = true;
  735. parent.title.text = child.title.text;
  736. parent.title.icon = child.title.icon;
  737. parent.title.className = child.title.className;
  738. // Mirror the parent title based on the child.
  739. child.title.changed.connect(() => {
  740. child.parent.title.text = child.title.text;
  741. child.parent.title.icon = child.title.icon;
  742. child.parent.title.className = child.title.className;
  743. });
  744. // Add the child widget to the parent widget.
  745. (parent.layout as PanelLayout).addChild(child);
  746. }
  747. /**
  748. * Ask the user whether to close an unsaved file.
  749. */
  750. private _maybeClose(widget: Widget, dirty: boolean): Promise<boolean> {
  751. if (!dirty) {
  752. return Promise.resolve(true);
  753. }
  754. let host = widget.isAttached ? widget.node : document.body;
  755. return showDialog({
  756. title: 'Close without saving?',
  757. body: `File "${widget.title.text}" has unsaved changes, close without saving?`,
  758. host
  759. }).then(value => {
  760. if (value && value.text === 'OK') {
  761. return true;
  762. }
  763. return false;
  764. });
  765. }
  766. /**
  767. * Clean up the data associated with a widget.
  768. */
  769. private _cleanupWidget(widget: Widget): void {
  770. // Remove the widget from our internal storage.
  771. let id = Private.contextProperty.get(widget);
  772. let index = this._widgets[id].indexOf(widget);
  773. this._widgets[id] = this._widgets[id].splice(index, 1);
  774. this._closeGuard = true;
  775. // If this is the last widget in that context, remove the context.
  776. if (!this._widgets[id]) {
  777. let session = this._contextManager.removeContext(id);
  778. if (session) {
  779. // TODO: show a dialog asking whether to shut down the kernel.
  780. widget.close();
  781. widget.dispose();
  782. }
  783. } else {
  784. widget.close();
  785. widget.dispose();
  786. }
  787. }
  788. /**
  789. * Get the appropriate widget factory by name.
  790. */
  791. private _getWidgetFactoryEx(widgetName: string): Private.IWidgetFactoryEx {
  792. let options: Private.IWidgetFactoryEx;
  793. if (widgetName === 'default') {
  794. options = this._widgetFactories[this._defaultWidgetFactory];
  795. } else {
  796. options = this._widgetFactories[widgetName];
  797. }
  798. return options;
  799. }
  800. /**
  801. * Get the appropriate model factory given a widget factory.
  802. */
  803. private _getModelFactoryEx(widgetName: string): Private.IModelFactoryEx {
  804. let wFactoryEx = this._getWidgetFactoryEx(widgetName);
  805. if (!wFactoryEx) {
  806. return;
  807. }
  808. return this._modelFactories[wFactoryEx.modelName];
  809. }
  810. private _modelFactories: { [key: string]: Private.IModelFactoryEx } = Object.create(null);
  811. private _widgetFactories: { [key: string]: Private.IWidgetFactoryEx } = Object.create(null);
  812. private _defaultWidgetFactory = '';
  813. private _defaultWidgetFactories: { [key: string]: string } = Object.create(null);
  814. private _widgets: { [key: string]: Widget[] } = Object.create(null);
  815. private _contentsManager: IContentsManager = null;
  816. private _sessionManager: INotebookSessionManager = null;
  817. private _contextManager: ContextManager = null;
  818. private _closeGuard = false;
  819. }
  820. /**
  821. * A private namespace for DocumentManager data.
  822. */
  823. namespace Private {
  824. /**
  825. * A signal emitted when a file is opened.
  826. */
  827. export
  828. const openedSignal = new Signal<DocumentManager, Widget>();
  829. /**
  830. * An extended interface for a model factory and its options.
  831. */
  832. export
  833. interface IModelFactoryEx extends IModelFactoryOptions {
  834. factory: IModelFactory;
  835. }
  836. /**
  837. * An extended interface for a widget factory and its options.
  838. */
  839. export
  840. interface IWidgetFactoryEx extends IWidgetFactoryOptions {
  841. factory: IWidgetFactory<Widget>;
  842. }
  843. /**
  844. * The widget factory name used to create a widget.
  845. */
  846. export
  847. const nameProperty = new Property<Widget, string>({
  848. name: 'name'
  849. });
  850. /**
  851. * The context id associated with a widget.
  852. */
  853. export
  854. const contextProperty = new Property<Widget, string>({
  855. name: 'context'
  856. });
  857. }