1 |
|
2 |
|
3 | import { Dialog, showDialog } from '@jupyterlab/apputils';
|
4 | import { Time } from '@jupyterlab/coreutils';
|
5 | import { nullTranslator } from '@jupyterlab/translation';
|
6 | import { ArrayExt, find } from '@lumino/algorithm';
|
7 | import { DisposableSet } from '@lumino/disposable';
|
8 | import { MessageLoop } from '@lumino/messaging';
|
9 | import { AttachedProperty } from '@lumino/properties';
|
10 | import { Signal } from '@lumino/signaling';
|
11 |
|
12 |
|
13 |
|
14 | const DOCUMENT_CLASS = 'jp-Document';
|
15 |
|
16 |
|
17 |
|
18 | export class DocumentWidgetManager {
|
19 | |
20 |
|
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 | this._recentsManager = options.recentsManager || null;
|
30 | }
|
31 | |
32 |
|
33 |
|
34 | get activateRequested() {
|
35 | return this._activateRequested;
|
36 | }
|
37 | |
38 |
|
39 |
|
40 | get confirmClosingDocument() {
|
41 | return this._confirmClosingTab;
|
42 | }
|
43 | set confirmClosingDocument(v) {
|
44 | if (this._confirmClosingTab !== v) {
|
45 | const oldValue = this._confirmClosingTab;
|
46 | this._confirmClosingTab = v;
|
47 | this._stateChanged.emit({
|
48 | name: 'confirmClosingDocument',
|
49 | oldValue,
|
50 | newValue: v
|
51 | });
|
52 | }
|
53 | }
|
54 | |
55 |
|
56 |
|
57 | get stateChanged() {
|
58 | return this._stateChanged;
|
59 | }
|
60 | |
61 |
|
62 |
|
63 | get isDisposed() {
|
64 | return this._isDisposed;
|
65 | }
|
66 | |
67 |
|
68 |
|
69 | dispose() {
|
70 | if (this.isDisposed) {
|
71 | return;
|
72 | }
|
73 | this._isDisposed = true;
|
74 | Signal.disconnectReceiver(this);
|
75 | }
|
76 | |
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | createWidget(factory, context) {
|
88 | const widget = factory.createNew(context);
|
89 | this._initializeWidget(widget, factory, context);
|
90 | return widget;
|
91 | }
|
92 | |
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | _initializeWidget(widget, factory, context) {
|
99 | Private.factoryProperty.set(widget, factory);
|
100 |
|
101 | const disposables = new DisposableSet();
|
102 | for (const extender of this._registry.widgetExtensions(factory.name)) {
|
103 | const disposable = extender.createNew(widget, context);
|
104 | if (disposable) {
|
105 | disposables.add(disposable);
|
106 | }
|
107 | }
|
108 | Private.disposablesProperty.set(widget, disposables);
|
109 | widget.disposed.connect(this._onWidgetDisposed, this);
|
110 | this.adoptWidget(context, widget);
|
111 | context.fileChanged.connect(this._onFileChanged, this);
|
112 | context.pathChanged.connect(this._onPathChanged, this);
|
113 | void context.ready.then(() => {
|
114 | void this.setCaption(widget);
|
115 | });
|
116 | }
|
117 | |
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | adoptWidget(context, widget) {
|
126 | const widgets = Private.widgetsProperty.get(context);
|
127 | widgets.push(widget);
|
128 | MessageLoop.installMessageHook(widget, this);
|
129 | widget.addClass(DOCUMENT_CLASS);
|
130 | widget.title.closable = true;
|
131 | widget.disposed.connect(this._widgetDisposed, this);
|
132 | Private.contextProperty.set(widget, context);
|
133 | }
|
134 | |
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | findWidget(context, widgetName) {
|
146 | const widgets = Private.widgetsProperty.get(context);
|
147 | if (!widgets) {
|
148 | return undefined;
|
149 | }
|
150 | return find(widgets, widget => {
|
151 | const factory = Private.factoryProperty.get(widget);
|
152 | if (!factory) {
|
153 | return false;
|
154 | }
|
155 | return factory.name === widgetName;
|
156 | });
|
157 | }
|
158 | |
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | contextForWidget(widget) {
|
166 | return Private.contextProperty.get(widget);
|
167 | }
|
168 | |
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | cloneWidget(widget) {
|
180 | const context = Private.contextProperty.get(widget);
|
181 | if (!context) {
|
182 | return undefined;
|
183 | }
|
184 | const factory = Private.factoryProperty.get(widget);
|
185 | if (!factory) {
|
186 | return undefined;
|
187 | }
|
188 | const newWidget = factory.createNew(context, widget);
|
189 | this._initializeWidget(newWidget, factory, context);
|
190 | return newWidget;
|
191 | }
|
192 | |
193 |
|
194 |
|
195 |
|
196 |
|
197 | closeWidgets(context) {
|
198 | const widgets = Private.widgetsProperty.get(context);
|
199 | return Promise.all(widgets.map(widget => this.onClose(widget))).then(() => undefined);
|
200 | }
|
201 | |
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | deleteWidgets(context) {
|
208 | const widgets = Private.widgetsProperty.get(context);
|
209 | return Promise.all(widgets.map(widget => this.onDelete(widget))).then(() => undefined);
|
210 | }
|
211 | |
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | messageHook(handler, msg) {
|
222 | switch (msg.type) {
|
223 | case 'close-request':
|
224 | void this.onClose(handler);
|
225 | return false;
|
226 | case 'activate-request': {
|
227 | const widget = handler;
|
228 | const context = this.contextForWidget(widget);
|
229 | if (context) {
|
230 | context.ready
|
231 | .then(() => {
|
232 |
|
233 | this._recordAsRecentlyOpened(widget, context.contentsModel);
|
234 | })
|
235 | .catch(() => {
|
236 | console.warn('Could not record the recents status for', context);
|
237 | });
|
238 | this._activateRequested.emit(context.path);
|
239 | }
|
240 | break;
|
241 | }
|
242 | default:
|
243 | break;
|
244 | }
|
245 | return true;
|
246 | }
|
247 | |
248 |
|
249 |
|
250 |
|
251 |
|
252 | async setCaption(widget) {
|
253 | const trans = this.translator.load('jupyterlab');
|
254 | const context = Private.contextProperty.get(widget);
|
255 | if (!context) {
|
256 | return;
|
257 | }
|
258 | const model = context.contentsModel;
|
259 | if (!model) {
|
260 | widget.title.caption = '';
|
261 | return;
|
262 | }
|
263 | return context
|
264 | .listCheckpoints()
|
265 | .then((checkpoints) => {
|
266 | if (widget.isDisposed) {
|
267 | return;
|
268 | }
|
269 | const last = checkpoints[checkpoints.length - 1];
|
270 | const checkpoint = last ? Time.format(last.last_modified) : 'None';
|
271 | let caption = trans.__('Name: %1\nPath: %2\n', model.name, model.path);
|
272 | if (context.model.readOnly) {
|
273 | caption += trans.__('Read-only');
|
274 | }
|
275 | else {
|
276 | caption +=
|
277 | trans.__('Last Saved: %1\n', Time.format(model.last_modified)) +
|
278 | trans.__('Last Checkpoint: %1', checkpoint);
|
279 | }
|
280 | widget.title.caption = caption;
|
281 | });
|
282 | }
|
283 | |
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 | async onClose(widget) {
|
291 | var _a;
|
292 |
|
293 | const [shouldClose, ignoreSave] = await this._maybeClose(widget, this.translator);
|
294 | if (widget.isDisposed) {
|
295 | return true;
|
296 | }
|
297 | if (shouldClose) {
|
298 | const context = Private.contextProperty.get(widget);
|
299 | if (!ignoreSave) {
|
300 | if (!context) {
|
301 | return true;
|
302 | }
|
303 | if ((_a = context.contentsModel) === null || _a === void 0 ? void 0 : _a.writable) {
|
304 | await context.save();
|
305 | }
|
306 | else {
|
307 | await context.saveAs();
|
308 | }
|
309 | }
|
310 | if (context) {
|
311 | const result = await Promise.race([
|
312 | context.ready,
|
313 | new Promise(resolve => setTimeout(resolve, 3000, 'timeout'))
|
314 | ]);
|
315 | if (result === 'timeout') {
|
316 | console.warn('Could not record the widget as recently closed because the context did not become ready in 3 seconds');
|
317 | }
|
318 | else {
|
319 |
|
320 |
|
321 |
|
322 | this._recordAsRecentlyClosed(widget, context.contentsModel);
|
323 | }
|
324 | }
|
325 | if (widget.isDisposed) {
|
326 | return true;
|
327 | }
|
328 | widget.dispose();
|
329 | }
|
330 | return shouldClose;
|
331 | }
|
332 | |
333 |
|
334 |
|
335 |
|
336 |
|
337 | onDelete(widget) {
|
338 | widget.dispose();
|
339 | return Promise.resolve(void 0);
|
340 | }
|
341 | |
342 |
|
343 |
|
344 | _recordAsRecentlyOpened(widget, model) {
|
345 | var _a;
|
346 | const recents = this._recentsManager;
|
347 | if (!recents) {
|
348 |
|
349 | return;
|
350 | }
|
351 | const path = model.path;
|
352 | const fileType = this._registry.getFileTypeForModel(model);
|
353 | const contentType = fileType.contentType;
|
354 | const factory = (_a = Private.factoryProperty.get(widget)) === null || _a === void 0 ? void 0 : _a.name;
|
355 | recents.addRecent({ path, contentType, factory }, 'opened');
|
356 |
|
357 | if (contentType !== 'directory') {
|
358 | const parent = path.lastIndexOf('/') > 0 ? path.slice(0, path.lastIndexOf('/')) : '';
|
359 | recents.addRecent({ path: parent, contentType: 'directory' }, 'opened');
|
360 | }
|
361 | }
|
362 | |
363 |
|
364 |
|
365 | _recordAsRecentlyClosed(widget, model) {
|
366 | var _a;
|
367 | const recents = this._recentsManager;
|
368 | if (!recents) {
|
369 |
|
370 | return;
|
371 | }
|
372 | const path = model.path;
|
373 | const fileType = this._registry.getFileTypeForModel(model);
|
374 | const contentType = fileType.contentType;
|
375 | const factory = (_a = Private.factoryProperty.get(widget)) === null || _a === void 0 ? void 0 : _a.name;
|
376 | recents.addRecent({ path, contentType, factory }, 'closed');
|
377 | }
|
378 | |
379 |
|
380 |
|
381 | async _maybeClose(widget, translator) {
|
382 | var _a, _b;
|
383 | translator = translator || nullTranslator;
|
384 | const trans = translator.load('jupyterlab');
|
385 |
|
386 | const context = Private.contextProperty.get(widget);
|
387 | if (!context) {
|
388 | return Promise.resolve([true, true]);
|
389 | }
|
390 | let widgets = Private.widgetsProperty.get(context);
|
391 | if (!widgets) {
|
392 | return Promise.resolve([true, true]);
|
393 | }
|
394 |
|
395 | widgets = widgets.filter(widget => {
|
396 | const factory = Private.factoryProperty.get(widget);
|
397 | if (!factory) {
|
398 | return false;
|
399 | }
|
400 | return factory.readOnly === false;
|
401 | });
|
402 | const fileName = widget.title.label;
|
403 | const factory = Private.factoryProperty.get(widget);
|
404 | const isDirty = context.model.dirty &&
|
405 | widgets.length <= 1 &&
|
406 | !((_a = factory === null || factory === void 0 ? void 0 : factory.readOnly) !== null && _a !== void 0 ? _a : true);
|
407 |
|
408 | if (this.confirmClosingDocument) {
|
409 | const buttons = [
|
410 | Dialog.cancelButton(),
|
411 | Dialog.okButton({
|
412 | label: isDirty ? trans.__('Close and save') : trans.__('Close'),
|
413 | ariaLabel: isDirty
|
414 | ? trans.__('Close and save Document')
|
415 | : trans.__('Close Document')
|
416 | })
|
417 | ];
|
418 | if (isDirty) {
|
419 | buttons.splice(1, 0, Dialog.warnButton({
|
420 | label: trans.__('Close without saving'),
|
421 | ariaLabel: trans.__('Close Document without saving')
|
422 | }));
|
423 | }
|
424 | const confirm = await showDialog({
|
425 | title: trans.__('Confirmation'),
|
426 | body: trans.__('Please confirm you want to close "%1".', fileName),
|
427 | checkbox: isDirty
|
428 | ? null
|
429 | : {
|
430 | label: trans.__('Do not ask me again.'),
|
431 | caption: trans.__('If checked, no confirmation to close a document will be asked in the future.')
|
432 | },
|
433 | buttons
|
434 | });
|
435 | if (confirm.isChecked) {
|
436 | this.confirmClosingDocument = false;
|
437 | }
|
438 | return Promise.resolve([
|
439 | confirm.button.accept,
|
440 | isDirty ? confirm.button.displayType === 'warn' : true
|
441 | ]);
|
442 | }
|
443 | else {
|
444 | if (!isDirty) {
|
445 | return Promise.resolve([true, true]);
|
446 | }
|
447 | const saveLabel = ((_b = context.contentsModel) === null || _b === void 0 ? void 0 : _b.writable)
|
448 | ? trans.__('Save')
|
449 | : trans.__('Save as');
|
450 | const result = await showDialog({
|
451 | title: trans.__('Save your work'),
|
452 | body: trans.__('Save changes in "%1" before closing?', fileName),
|
453 | buttons: [
|
454 | Dialog.cancelButton(),
|
455 | Dialog.warnButton({
|
456 | label: trans.__('Discard'),
|
457 | ariaLabel: trans.__('Discard changes to file')
|
458 | }),
|
459 | Dialog.okButton({ label: saveLabel })
|
460 | ]
|
461 | });
|
462 | return [result.button.accept, result.button.displayType === 'warn'];
|
463 | }
|
464 | }
|
465 | |
466 |
|
467 |
|
468 | _widgetDisposed(widget) {
|
469 | const context = Private.contextProperty.get(widget);
|
470 | if (!context) {
|
471 | return;
|
472 | }
|
473 | const widgets = Private.widgetsProperty.get(context);
|
474 | if (!widgets) {
|
475 | return;
|
476 | }
|
477 |
|
478 | ArrayExt.removeFirstOf(widgets, widget);
|
479 |
|
480 | if (!widgets.length) {
|
481 | context.dispose();
|
482 | }
|
483 | }
|
484 | |
485 |
|
486 |
|
487 | _onWidgetDisposed(widget) {
|
488 | const disposables = Private.disposablesProperty.get(widget);
|
489 | disposables.dispose();
|
490 | }
|
491 | |
492 |
|
493 |
|
494 | _onFileChanged(context) {
|
495 | const widgets = Private.widgetsProperty.get(context);
|
496 | for (const widget of widgets) {
|
497 | void this.setCaption(widget);
|
498 | }
|
499 | }
|
500 | |
501 |
|
502 |
|
503 | _onPathChanged(context) {
|
504 | const widgets = Private.widgetsProperty.get(context);
|
505 | for (const widget of widgets) {
|
506 | void this.setCaption(widget);
|
507 | }
|
508 | }
|
509 | }
|
510 |
|
511 |
|
512 |
|
513 | var Private;
|
514 | (function (Private) {
|
515 | |
516 |
|
517 |
|
518 | Private.contextProperty = new AttachedProperty({
|
519 | name: 'context',
|
520 | create: () => undefined
|
521 | });
|
522 | |
523 |
|
524 |
|
525 | Private.factoryProperty = new AttachedProperty({
|
526 | name: 'factory',
|
527 | create: () => undefined
|
528 | });
|
529 | |
530 |
|
531 |
|
532 | Private.widgetsProperty = new AttachedProperty({
|
533 | name: 'widgets',
|
534 | create: () => []
|
535 | });
|
536 | |
537 |
|
538 |
|
539 | Private.disposablesProperty = new AttachedProperty({
|
540 | name: 'disposables',
|
541 | create: () => new DisposableSet()
|
542 | });
|
543 | })(Private || (Private = {}));
|
544 |
|
\ | No newline at end of file |