UNPKG

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