UNPKG

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