1 |
|
2 |
|
3 | import { Dialog, showDialog } from '@jupyterlab/apputils';
|
4 | import { Time } from '@jupyterlab/coreutils';
|
5 | import { nullTranslator } from '@jupyterlab/translation';
|
6 | import { ArrayExt, each, filter, find, map, toArray } 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._isDisposed = false;
|
25 | this._registry = options.registry;
|
26 | this.translator = options.translator || nullTranslator;
|
27 | }
|
28 | |
29 |
|
30 |
|
31 | get activateRequested() {
|
32 | return this._activateRequested;
|
33 | }
|
34 | |
35 |
|
36 |
|
37 | get isDisposed() {
|
38 | return this._isDisposed;
|
39 | }
|
40 | |
41 |
|
42 |
|
43 | dispose() {
|
44 | if (this.isDisposed) {
|
45 | return;
|
46 | }
|
47 | this._isDisposed = true;
|
48 | Signal.disconnectReceiver(this);
|
49 | }
|
50 | |
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | createWidget(factory, context) {
|
62 | const widget = factory.createNew(context);
|
63 | this._initializeWidget(widget, factory, context);
|
64 | return widget;
|
65 | }
|
66 | |
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | _initializeWidget(widget, factory, context) {
|
73 | Private.factoryProperty.set(widget, factory);
|
74 |
|
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 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
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 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
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 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | contextForWidget(widget) {
|
140 | return Private.contextProperty.get(widget);
|
141 | }
|
142 | |
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
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 |
|
168 |
|
169 |
|
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 |
|
177 |
|
178 |
|
179 |
|
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 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
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 |
|
214 |
|
215 |
|
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 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 | async onClose(widget) {
|
256 | var _a;
|
257 |
|
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 |
|
284 |
|
285 |
|
286 |
|
287 | onDelete(widget) {
|
288 | widget.dispose();
|
289 | return Promise.resolve(void 0);
|
290 | }
|
291 | |
292 |
|
293 |
|
294 | _maybeClose(widget, translator) {
|
295 | var _a;
|
296 | translator = translator || nullTranslator;
|
297 | const trans = translator.load('jupyterlab');
|
298 |
|
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 |
|
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 |
|
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 |
|
351 | ArrayExt.removeFirstOf(widgets, widget);
|
352 |
|
353 | if (!widgets.length) {
|
354 | context.dispose();
|
355 | }
|
356 | }
|
357 | |
358 |
|
359 |
|
360 | _onWidgetDisposed(widget) {
|
361 | const disposables = Private.disposablesProperty.get(widget);
|
362 | disposables.dispose();
|
363 | }
|
364 | |
365 |
|
366 |
|
367 | _onFileChanged(context) {
|
368 | const widgets = Private.widgetsProperty.get(context);
|
369 | each(widgets, widget => {
|
370 | void this.setCaption(widget);
|
371 | });
|
372 | }
|
373 | |
374 |
|
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 |
|
385 |
|
386 | var Private;
|
387 | (function (Private) {
|
388 | |
389 |
|
390 |
|
391 | Private.contextProperty = new AttachedProperty({
|
392 | name: 'context',
|
393 | create: () => undefined
|
394 | });
|
395 | |
396 |
|
397 |
|
398 | Private.factoryProperty = new AttachedProperty({
|
399 | name: 'factory',
|
400 | create: () => undefined
|
401 | });
|
402 | |
403 |
|
404 |
|
405 | Private.widgetsProperty = new AttachedProperty({
|
406 | name: 'widgets',
|
407 | create: () => []
|
408 | });
|
409 | |
410 |
|
411 |
|
412 | Private.disposablesProperty = new AttachedProperty({
|
413 | name: 'disposables',
|
414 | create: () => new DisposableSet()
|
415 | });
|
416 | })(Private || (Private = {}));
|
417 |
|
\ | No newline at end of file |