widgetmanager.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ArrayExt, each, map, find, filter, toArray
  5. } from '@phosphor/algorithm';
  6. import {
  7. DisposableSet, IDisposable
  8. } from '@phosphor/disposable';
  9. import {
  10. IMessageHandler, Message, MessageLoop
  11. } from '@phosphor/messaging';
  12. import {
  13. AttachedProperty
  14. } from '@phosphor/properties';
  15. import {
  16. ISignal, Signal
  17. } from '@phosphor/signaling';
  18. import {
  19. Widget
  20. } from '@phosphor/widgets';
  21. import {
  22. Time
  23. } from '@jupyterlab/coreutils';
  24. import {
  25. showDialog, Dialog
  26. } from '@jupyterlab/apputils';
  27. import {
  28. DocumentRegistry
  29. } from '@jupyterlab/docregistry';
  30. /**
  31. * The class name added to document widgets.
  32. */
  33. const DOCUMENT_CLASS = 'jp-Document';
  34. /**
  35. * A class that maintains the lifecyle of file-backed widgets.
  36. */
  37. export
  38. class DocumentWidgetManager implements IDisposable {
  39. /**
  40. * Construct a new document widget manager.
  41. */
  42. constructor(options: DocumentWidgetManager.IOptions) {
  43. this._registry = options.registry;
  44. }
  45. /**
  46. * A signal emitted when one of the documents is activated.
  47. */
  48. get activateRequested(): ISignal<this, string> {
  49. return this._activateRequested;
  50. }
  51. /**
  52. * Test whether the document widget manager is disposed.
  53. */
  54. get isDisposed(): boolean {
  55. return this._registry === null;
  56. }
  57. /**
  58. * Dispose of the resources used by the widget manager.
  59. */
  60. dispose(): void {
  61. if (this._registry == null) {
  62. return;
  63. }
  64. this._registry = null;
  65. Signal.disconnectReceiver(this);
  66. }
  67. /**
  68. * Create a widget for a document and handle its lifecycle.
  69. *
  70. * @param factory - The widget factory.
  71. *
  72. * @param context - The document context object.
  73. *
  74. * @returns A widget created by the factory.
  75. *
  76. * @throws If the factory is not registered.
  77. */
  78. createWidget(factory: DocumentRegistry.WidgetFactory, context: DocumentRegistry.Context): Widget {
  79. let widget = factory.createNew(context);
  80. Private.factoryProperty.set(widget, factory);
  81. // Handle widget extensions.
  82. let disposables = new DisposableSet();
  83. each(this._registry.widgetExtensions(factory.name), extender => {
  84. disposables.add(extender.createNew(widget, context));
  85. });
  86. Private.disposablesProperty.set(widget, disposables);
  87. widget.disposed.connect(this._onWidgetDisposed, this);
  88. this.adoptWidget(context, widget);
  89. context.fileChanged.connect(this._onFileChanged, this);
  90. context.pathChanged.connect(this._onPathChanged, this);
  91. context.ready.then(() => {
  92. this.setCaption(widget);
  93. });
  94. return widget;
  95. }
  96. /**
  97. * Install the message hook for the widget and add to list
  98. * of known widgets.
  99. *
  100. * @param context - The document context object.
  101. *
  102. * @param widget - The widget to adopt.
  103. */
  104. adoptWidget(context: DocumentRegistry.Context, widget: Widget): void {
  105. let widgets = Private.widgetsProperty.get(context);
  106. widgets.push(widget);
  107. MessageLoop.installMessageHook(widget, (handler: IMessageHandler, msg: Message) => {
  108. return this.filterMessage(handler, msg);
  109. });
  110. widget.addClass(DOCUMENT_CLASS);
  111. widget.title.closable = true;
  112. widget.disposed.connect(this._widgetDisposed, this);
  113. Private.contextProperty.set(widget, context);
  114. }
  115. /**
  116. * See if a widget already exists for the given context and widget name.
  117. *
  118. * @param context - The document context object.
  119. *
  120. * @returns The found widget, or `undefined`.
  121. *
  122. * #### Notes
  123. * This can be used to use an existing widget instead of opening
  124. * a new widget.
  125. */
  126. findWidget(context: DocumentRegistry.Context, widgetName: string): Widget {
  127. let widgets = Private.widgetsProperty.get(context);
  128. return find(widgets, widget => {
  129. let name = Private.factoryProperty.get(widget).name;
  130. if (name === widgetName) {
  131. return true;
  132. }
  133. });
  134. }
  135. /**
  136. * Get the document context for a widget.
  137. *
  138. * @param widget - The widget of interest.
  139. *
  140. * @returns The context associated with the widget, or `undefined`.
  141. */
  142. contextForWidget(widget: Widget): DocumentRegistry.Context {
  143. return !widget ? void 0 : Private.contextProperty.get(widget);
  144. }
  145. /**
  146. * Clone a widget.
  147. *
  148. * @param widget - The source widget.
  149. *
  150. * @returns A new widget or `undefined`.
  151. *
  152. * #### Notes
  153. * Uses the same widget factory and context as the source, or returns
  154. * `undefined` if the source widget is not managed by this manager.
  155. */
  156. cloneWidget(widget: Widget): Widget {
  157. let context = Private.contextProperty.get(widget);
  158. if (!context) {
  159. return;
  160. }
  161. let factory = Private.factoryProperty.get(widget);
  162. let newWidget = this.createWidget(factory, context);
  163. this.adoptWidget(context, newWidget);
  164. return widget;
  165. }
  166. /**
  167. * Close the widgets associated with a given context.
  168. *
  169. * @param context - The document context object.
  170. */
  171. closeWidgets(context: DocumentRegistry.Context): Promise<void> {
  172. let widgets = Private.widgetsProperty.get(context);
  173. return Promise.all(
  174. toArray(map(widgets, widget => this.onClose(widget)))
  175. ).then(() => undefined);
  176. }
  177. /**
  178. * Filter a message sent to a message handler.
  179. *
  180. * @param handler - The target handler of the message.
  181. *
  182. * @param msg - The message dispatched to the handler.
  183. *
  184. * @returns `false` if the message should be filtered, of `true`
  185. * if the message should be dispatched to the handler as normal.
  186. */
  187. protected filterMessage(handler: IMessageHandler, msg: Message): boolean {
  188. switch (msg.type) {
  189. case 'close-request':
  190. if (this._closeGuard) {
  191. return true;
  192. }
  193. this.onClose(handler as Widget);
  194. return false;
  195. case 'activate-request':
  196. let context = this.contextForWidget(handler as Widget);
  197. this._activateRequested.emit(context.path);
  198. break;
  199. default:
  200. break;
  201. }
  202. return true;
  203. }
  204. /**
  205. * Set the caption for widget title.
  206. *
  207. * @param widget - The target widget.
  208. */
  209. protected setCaption(widget: Widget): void {
  210. let context = Private.contextProperty.get(widget);
  211. let model = context.contentsModel;
  212. if (!model) {
  213. widget.title.caption = '';
  214. return;
  215. }
  216. context.listCheckpoints().then(checkpoints => {
  217. if (widget.isDisposed) {
  218. return;
  219. }
  220. let last = checkpoints[checkpoints.length - 1];
  221. let checkpoint = last ? Time.format(last.last_modified) : 'None';
  222. widget.title.caption = (
  223. `Name: ${model.name}\n` +
  224. `Path: ${model.path}\n` +
  225. `Last Saved: ${Time.format(model.last_modified)}\n` +
  226. `Last Checkpoint: ${checkpoint}`
  227. );
  228. });
  229. }
  230. /**
  231. * Handle `'close-request'` messages.
  232. *
  233. * @param widget - The target widget.
  234. *
  235. * @returns A promise that resolves with whether the widget was closed.
  236. */
  237. protected onClose(widget: Widget): Promise<boolean> {
  238. // Handle dirty state.
  239. return this._maybeClose(widget).then(result => {
  240. if (widget.isDisposed) {
  241. return true;
  242. }
  243. if (result) {
  244. this._closeGuard = true;
  245. widget.close();
  246. this._closeGuard = false;
  247. // Dispose of document widgets when they are closed.
  248. widget.dispose();
  249. }
  250. return result;
  251. }).catch(() => {
  252. widget.dispose();
  253. });
  254. }
  255. /**
  256. * Ask the user whether to close an unsaved file.
  257. */
  258. private _maybeClose(widget: Widget): Promise<boolean> {
  259. // Bail if the model is not dirty or other widgets are using the model.)
  260. let context = Private.contextProperty.get(widget);
  261. if (!context) {
  262. return Promise.resolve(true);
  263. }
  264. let widgets = Private.widgetsProperty.get(context);
  265. // Filter by whether the factories are read only.
  266. widgets = toArray(filter(widgets, widget => {
  267. let factory = Private.factoryProperty.get(widget);
  268. return factory.readOnly === false;
  269. }));
  270. let factory = Private.factoryProperty.get(widget);
  271. let model = context.model;
  272. if (!model.dirty || widgets.length > 1 || factory.readOnly) {
  273. return Promise.resolve(true);
  274. }
  275. let fileName = widget.title.label;
  276. return showDialog({
  277. title: 'Close without saving?',
  278. body: `File "${fileName}" has unsaved changes, close without saving?`,
  279. buttons: [Dialog.cancelButton(), Dialog.warnButton()]
  280. }).then(value => {
  281. return value.accept;
  282. });
  283. }
  284. /**
  285. * Handle the disposal of a widget.
  286. */
  287. private _widgetDisposed(widget: Widget): void {
  288. let context = Private.contextProperty.get(widget);
  289. let widgets = Private.widgetsProperty.get(context);
  290. // Remove the widget.
  291. ArrayExt.removeFirstOf(widgets, widget);
  292. // Dispose of the context if this is the last widget using it.
  293. if (!widgets.length) {
  294. context.dispose();
  295. }
  296. }
  297. /**
  298. * Handle the disposal of a widget.
  299. */
  300. private _onWidgetDisposed(widget: Widget): void {
  301. let disposables = Private.disposablesProperty.get(widget);
  302. disposables.dispose();
  303. }
  304. /**
  305. * Handle a file changed signal for a context.
  306. */
  307. private _onFileChanged(context: DocumentRegistry.Context): void {
  308. let widgets = Private.widgetsProperty.get(context);
  309. each(widgets, widget => { this.setCaption(widget); });
  310. }
  311. /**
  312. * Handle a path changed signal for a context.
  313. */
  314. private _onPathChanged(context: DocumentRegistry.Context): void {
  315. let widgets = Private.widgetsProperty.get(context);
  316. each(widgets, widget => { this.setCaption(widget); });
  317. }
  318. private _closeGuard = false;
  319. private _registry: DocumentRegistry = null;
  320. private _activateRequested = new Signal<this, string>(this);
  321. }
  322. /**
  323. * A namespace for document widget manager statics.
  324. */
  325. export
  326. namespace DocumentWidgetManager {
  327. /**
  328. * The options used to initialize a document widget manager.
  329. */
  330. export
  331. interface IOptions {
  332. /**
  333. * A document registry instance.
  334. */
  335. registry: DocumentRegistry;
  336. }
  337. }
  338. /**
  339. * A private namespace for DocumentManager data.
  340. */
  341. namespace Private {
  342. /**
  343. * A private attached property for a widget context.
  344. */
  345. export
  346. const contextProperty = new AttachedProperty<Widget, DocumentRegistry.Context>({
  347. name: 'context',
  348. create: () => null
  349. });
  350. /**
  351. * A private attached property for a widget factory.
  352. */
  353. export
  354. const factoryProperty = new AttachedProperty<Widget, DocumentRegistry.WidgetFactory> ({
  355. name: 'factory',
  356. create: () => null
  357. });
  358. /**
  359. * A private attached property for the widgets associated with a context.
  360. */
  361. export
  362. const widgetsProperty = new AttachedProperty<DocumentRegistry.Context, Widget[]>({
  363. name: 'widgets',
  364. create: () => []
  365. });
  366. /**
  367. * A private attached property for a widget's disposables.
  368. */
  369. export
  370. const disposablesProperty = new AttachedProperty<Widget, DisposableSet>({
  371. name: 'disposables',
  372. create: () => new DisposableSet()
  373. });
  374. }