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 | }
|
30 | |
31 |
|
32 |
|
33 | get activateRequested() {
|
34 | return this._activateRequested;
|
35 | }
|
36 | |
37 |
|
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 |
|
55 |
|
56 | get stateChanged() {
|
57 | return this._stateChanged;
|
58 | }
|
59 | |
60 |
|
61 |
|
62 | get isDisposed() {
|
63 | return this._isDisposed;
|
64 | }
|
65 | |
66 |
|
67 |
|
68 | dispose() {
|
69 | if (this.isDisposed) {
|
70 | return;
|
71 | }
|
72 | this._isDisposed = true;
|
73 | Signal.disconnectReceiver(this);
|
74 | }
|
75 | |
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | createWidget(factory, context) {
|
87 | const widget = factory.createNew(context);
|
88 | this._initializeWidget(widget, factory, context);
|
89 | return widget;
|
90 | }
|
91 | |
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | _initializeWidget(widget, factory, context) {
|
98 | Private.factoryProperty.set(widget, factory);
|
99 |
|
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 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
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 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
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 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 | contextForWidget(widget) {
|
165 | return Private.contextProperty.get(widget);
|
166 | }
|
167 | |
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
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 |
|
193 |
|
194 |
|
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 |
|
202 |
|
203 |
|
204 |
|
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 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
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 |
|
239 |
|
240 |
|
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 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 | async onClose(widget) {
|
281 | var _a;
|
282 |
|
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 |
|
309 |
|
310 |
|
311 |
|
312 | onDelete(widget) {
|
313 | widget.dispose();
|
314 | return Promise.resolve(void 0);
|
315 | }
|
316 | |
317 |
|
318 |
|
319 | async _maybeClose(widget, translator) {
|
320 | var _a, _b;
|
321 | translator = translator || nullTranslator;
|
322 | const trans = translator.load('jupyterlab');
|
323 |
|
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 |
|
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 |
|
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 |
|
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 |
|
416 | ArrayExt.removeFirstOf(widgets, widget);
|
417 |
|
418 | if (!widgets.length) {
|
419 | context.dispose();
|
420 | }
|
421 | }
|
422 | |
423 |
|
424 |
|
425 | _onWidgetDisposed(widget) {
|
426 | const disposables = Private.disposablesProperty.get(widget);
|
427 | disposables.dispose();
|
428 | }
|
429 | |
430 |
|
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 |
|
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 |
|
450 |
|
451 | var Private;
|
452 | (function (Private) {
|
453 | |
454 |
|
455 |
|
456 | Private.contextProperty = new AttachedProperty({
|
457 | name: 'context',
|
458 | create: () => undefined
|
459 | });
|
460 | |
461 |
|
462 |
|
463 | Private.factoryProperty = new AttachedProperty({
|
464 | name: 'factory',
|
465 | create: () => undefined
|
466 | });
|
467 | |
468 |
|
469 |
|
470 | Private.widgetsProperty = new AttachedProperty({
|
471 | name: 'widgets',
|
472 | create: () => []
|
473 | });
|
474 | |
475 |
|
476 |
|
477 | Private.disposablesProperty = new AttachedProperty({
|
478 | name: 'disposables',
|
479 | create: () => new DisposableSet()
|
480 | });
|
481 | })(Private || (Private = {}));
|
482 |
|
\ | No newline at end of file |