widgetmanager.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IKernel
  5. } from 'jupyter-js-services';
  6. import {
  7. DisposableSet
  8. } from 'phosphor/lib/core/disposable';
  9. import {
  10. IMessageHandler, Message, installMessageHook
  11. } from 'phosphor/lib/core/messaging';
  12. import {
  13. AttachedProperty
  14. } from 'phosphor/lib/core/properties';
  15. import {
  16. Widget
  17. } from 'phosphor/lib/ui/widget';
  18. import {
  19. showDialog
  20. } from '../dialog';
  21. import {
  22. DocumentRegistry, IDocumentContext, IDocumentModel
  23. } from '../docregistry';
  24. import {
  25. ContextManager
  26. } from './context';
  27. /**
  28. * The class name added to document widgets.
  29. */
  30. const DOCUMENT_CLASS = 'jp-Document';
  31. /**
  32. * A class that maintains the lifecyle of file-backed widgets.
  33. */
  34. export
  35. class DocumentWidgetManager {
  36. /**
  37. * Construct a new document widget manager.
  38. */
  39. constructor(options: DocumentWidgetManager.IOptions) {
  40. this._contextManager = options.contextManager;
  41. this._registry = options.registry;
  42. }
  43. /**
  44. * Dispose of the resources used by the widget manager.
  45. */
  46. dispose(): void {
  47. this._registry = null;
  48. this._contextManager = null;
  49. for (let id in this._widgets) {
  50. for (let widget of this._widgets[id]) {
  51. widget.dispose();
  52. }
  53. }
  54. }
  55. /**
  56. * Create a widget for a document and handle its lifecycle.
  57. */
  58. createWidget(name: string, id: string, kernel?: IKernel.IModel): Widget {
  59. let factory = this._registry.getWidgetFactory(name);
  60. let context = this._contextManager.getContext(id);
  61. let widget = factory.createNew(context, kernel);
  62. Private.nameProperty.set(widget, name);
  63. // Handle widget extensions.
  64. let disposables = new DisposableSet();
  65. for (let extender of this._registry.getWidgetExtensions(name)) {
  66. disposables.add(extender.createNew(widget, context));
  67. }
  68. widget.disposed.connect(() => {
  69. disposables.dispose();
  70. });
  71. this.adoptWidget(id, widget);
  72. return widget;
  73. }
  74. /**
  75. * Install the message hook for the widget and add to list
  76. * of known widgets.
  77. */
  78. adoptWidget(id: string, widget: Widget): void {
  79. if (!(id in this._widgets)) {
  80. this._widgets[id] = [];
  81. }
  82. this._widgets[id].push(widget);
  83. installMessageHook(widget, (handler: IMessageHandler, msg: Message) => {
  84. return this.filterMessage(handler, msg);
  85. });
  86. widget.addClass(DOCUMENT_CLASS);
  87. widget.title.closable = true;
  88. widget.disposed.connect(() => {
  89. // Remove the widget from the widget registry.
  90. let index = this._widgets[id].indexOf(widget);
  91. this._widgets[id].splice(index, 1);
  92. // Dispose of the context if this is the last widget using it.
  93. if (!this._widgets[id].length) {
  94. this._contextManager.removeContext(id);
  95. }
  96. });
  97. Private.idProperty.set(widget, id);
  98. }
  99. /**
  100. * See if a widget already exists for the given path and widget name.
  101. *
  102. * #### Notes
  103. * This can be used to use an existing widget instead of opening
  104. * a new widget.
  105. */
  106. findWidget(path: string, widgetName: string): Widget {
  107. let ids = this._contextManager.getIdsForPath(path);
  108. for (let id of ids) {
  109. for (let widget of this._widgets[id]) {
  110. let name = Private.nameProperty.get(widget);
  111. if (name === widgetName) {
  112. return widget;
  113. }
  114. }
  115. }
  116. }
  117. /**
  118. * Get the document context for a widget.
  119. */
  120. contextForWidget(widget: Widget): IDocumentContext<IDocumentModel> {
  121. let id = Private.idProperty.get(widget);
  122. return this._contextManager.getContext(id);
  123. }
  124. /**
  125. * Clone a widget.
  126. *
  127. * #### Notes
  128. * This will create a new widget with the same model and context
  129. * as this widget.
  130. */
  131. clone(widget: Widget): Widget {
  132. let id = Private.idProperty.get(widget);
  133. let name = Private.nameProperty.get(widget);
  134. let newWidget = this.createWidget(name, id);
  135. this.adoptWidget(id, newWidget);
  136. return widget;
  137. }
  138. /**
  139. * Close the widgets associated with a given path.
  140. */
  141. closeFile(path: string): void {
  142. let ids = this._contextManager.getIdsForPath(path);
  143. for (let id of ids) {
  144. let widgets: Widget[] = this._widgets[id] || [];
  145. for (let w of widgets) {
  146. w.close();
  147. }
  148. }
  149. }
  150. /**
  151. * Close all of the open documents.
  152. */
  153. closeAll(): void {
  154. for (let id in this._widgets) {
  155. for (let w of this._widgets[id]) {
  156. w.close();
  157. }
  158. }
  159. }
  160. /**
  161. * Filter a message sent to a message handler.
  162. *
  163. * @param handler - The target handler of the message.
  164. *
  165. * @param msg - The message dispatched to the handler.
  166. *
  167. * @returns `true` if the message should be filtered, of `false`
  168. * if the message should be dispatched to the handler as normal.
  169. */
  170. protected filterMessage(handler: IMessageHandler, msg: Message): boolean {
  171. if (msg.type === 'close-request') {
  172. if (this._closeGuard) {
  173. return false;
  174. }
  175. this.onClose(handler as Widget);
  176. return true;
  177. }
  178. return false;
  179. }
  180. /**
  181. * Handle `'close-request'` messages.
  182. */
  183. protected onClose(widget: Widget): void {
  184. // Handle dirty state.
  185. this._maybeClose(widget).then(result => {
  186. if (result) {
  187. this._closeGuard = true;
  188. widget.close();
  189. this._closeGuard = false;
  190. // Dispose of document widgets when they are closed.
  191. widget.dispose();
  192. }
  193. }).catch(() => {
  194. widget.dispose();
  195. });
  196. }
  197. /**
  198. * Ask the user whether to close an unsaved file.
  199. */
  200. private _maybeClose(widget: Widget): Promise<boolean> {
  201. // Bail if the model is not dirty or other widgets are using the model.
  202. let id = Private.idProperty.get(widget);
  203. let widgets = this._widgets[id];
  204. let model = this._contextManager.getModel(id);
  205. if (!model.dirty || widgets.length > 1) {
  206. return Promise.resolve(true);
  207. }
  208. let fileName = widget.title.label;
  209. return showDialog({
  210. title: 'Close without saving?',
  211. body: `File "${fileName}" has unsaved changes, close without saving?`
  212. }).then(value => {
  213. if (value && value.text === 'OK') {
  214. return true;
  215. }
  216. return false;
  217. });
  218. }
  219. private _closeGuard = false;
  220. private _contextManager: ContextManager = null;
  221. private _registry: DocumentRegistry = null;
  222. private _widgets: { [key: string]: Widget[] } = Object.create(null);
  223. }
  224. /**
  225. * A namespace for document widget manager statics.
  226. */
  227. export
  228. namespace DocumentWidgetManager {
  229. /**
  230. * The options used to initialize a document widget manager.
  231. */
  232. export
  233. interface IOptions {
  234. /**
  235. * A document registry instance.
  236. */
  237. registry: DocumentRegistry;
  238. /**
  239. * A context manager instance.
  240. */
  241. contextManager: ContextManager;
  242. }
  243. }
  244. /**
  245. * A private namespace for DocumentManager data.
  246. */
  247. namespace Private {
  248. /**
  249. * A private attached property for a widget context id.
  250. */
  251. export
  252. const idProperty = new AttachedProperty<Widget, string>({
  253. name: 'id'
  254. });
  255. /**
  256. * A private attached property for a widget factory name.
  257. */
  258. export
  259. const nameProperty = new AttachedProperty<Widget, string>({
  260. name: 'name'
  261. });
  262. }