@jupyterlab/application 0.19.1-alpha.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/shell.js CHANGED
@@ -1,854 +1,857 @@
1
- "use strict";
2
- // Copyright (c) Jupyter Development Team.
3
- // Distributed under the terms of the Modified BSD License.
4
- Object.defineProperty(exports, "__esModule", { value: true });
5
- const algorithm_1 = require("@phosphor/algorithm");
6
- const coreutils_1 = require("@phosphor/coreutils");
7
- const messaging_1 = require("@phosphor/messaging");
8
- const signaling_1 = require("@phosphor/signaling");
9
- const widgets_1 = require("@phosphor/widgets");
10
- /**
11
- * The class name added to AppShell instances.
12
- */
13
- const APPLICATION_SHELL_CLASS = 'jp-ApplicationShell';
14
- /**
15
- * The class name added to side bar instances.
16
- */
17
- const SIDEBAR_CLASS = 'jp-SideBar';
18
- /**
19
- * The class name added to the current widget's title.
20
- */
21
- const CURRENT_CLASS = 'jp-mod-current';
22
- /**
23
- * The class name added to the active widget's title.
24
- */
25
- const ACTIVE_CLASS = 'jp-mod-active';
26
- /**
27
- * The default rank of items added to a sidebar.
28
- */
29
- const DEFAULT_RANK = 500;
30
- /**
31
- * The data attribute added to the document body indicating shell's mode.
32
- */
33
- const MODE_ATTRIBUTE = 'data-shell-mode';
34
- const ACTIVITY_CLASS = 'jp-Activity';
35
- /**
36
- * The application shell for JupyterLab.
37
- */
38
- class ApplicationShell extends widgets_1.Widget {
39
- /**
40
- * Construct a new application shell.
41
- */
42
- constructor() {
43
- super();
44
- /**
45
- * A message hook for child add/remove messages on the main area dock panel.
46
- */
47
- this._dockChildHook = (handler, msg) => {
48
- switch (msg.type) {
49
- case 'child-added':
50
- msg.child.addClass(ACTIVITY_CLASS);
51
- this._tracker.add(msg.child);
52
- break;
53
- case 'child-removed':
54
- msg.child.removeClass(ACTIVITY_CLASS);
55
- this._tracker.remove(msg.child);
56
- break;
57
- default:
58
- break;
59
- }
60
- return true;
61
- };
62
- this._activeChanged = new signaling_1.Signal(this);
63
- this._cachedLayout = null;
64
- this._currentChanged = new signaling_1.Signal(this);
65
- this._isRestored = false;
66
- this._layoutModified = new signaling_1.Signal(this);
67
- this._restored = new coreutils_1.PromiseDelegate();
68
- this._tracker = new widgets_1.FocusTracker();
69
- this._debouncer = 0;
70
- this._addOptionsCache = new Map();
71
- this._sideOptionsCache = new Map();
72
- this.addClass(APPLICATION_SHELL_CLASS);
73
- this.id = 'main';
74
- let bottomPanel = (this._bottomPanel = new widgets_1.BoxPanel());
75
- let topPanel = (this._topPanel = new widgets_1.Panel());
76
- let hboxPanel = new widgets_1.BoxPanel();
77
- let dockPanel = (this._dockPanel = new widgets_1.DockPanel());
78
- messaging_1.MessageLoop.installMessageHook(dockPanel, this._dockChildHook);
79
- let hsplitPanel = new widgets_1.SplitPanel();
80
- let leftHandler = (this._leftHandler = new Private.SideBarHandler('left'));
81
- let rightHandler = (this._rightHandler = new Private.SideBarHandler('right'));
82
- let rootLayout = new widgets_1.BoxLayout();
83
- bottomPanel.id = 'jp-bottom-panel';
84
- topPanel.id = 'jp-top-panel';
85
- hboxPanel.id = 'jp-main-content-panel';
86
- dockPanel.id = 'jp-main-dock-panel';
87
- hsplitPanel.id = 'jp-main-split-panel';
88
- leftHandler.sideBar.addClass(SIDEBAR_CLASS);
89
- leftHandler.sideBar.addClass('jp-mod-left');
90
- leftHandler.stackedPanel.id = 'jp-left-stack';
91
- rightHandler.sideBar.addClass(SIDEBAR_CLASS);
92
- rightHandler.sideBar.addClass('jp-mod-right');
93
- rightHandler.stackedPanel.id = 'jp-right-stack';
94
- bottomPanel.direction = 'bottom-to-top';
95
- hboxPanel.spacing = 0;
96
- dockPanel.spacing = 5;
97
- hsplitPanel.spacing = 1;
98
- hboxPanel.direction = 'left-to-right';
99
- hsplitPanel.orientation = 'horizontal';
100
- widgets_1.SplitPanel.setStretch(leftHandler.stackedPanel, 0);
101
- widgets_1.SplitPanel.setStretch(dockPanel, 1);
102
- widgets_1.SplitPanel.setStretch(rightHandler.stackedPanel, 0);
103
- widgets_1.BoxPanel.setStretch(leftHandler.sideBar, 0);
104
- widgets_1.BoxPanel.setStretch(hsplitPanel, 1);
105
- widgets_1.BoxPanel.setStretch(rightHandler.sideBar, 0);
106
- hsplitPanel.addWidget(leftHandler.stackedPanel);
107
- hsplitPanel.addWidget(dockPanel);
108
- hsplitPanel.addWidget(rightHandler.stackedPanel);
109
- hboxPanel.addWidget(leftHandler.sideBar);
110
- hboxPanel.addWidget(hsplitPanel);
111
- hboxPanel.addWidget(rightHandler.sideBar);
112
- rootLayout.direction = 'top-to-bottom';
113
- rootLayout.spacing = 0; // TODO make this configurable?
114
- widgets_1.BoxLayout.setStretch(topPanel, 0);
115
- widgets_1.BoxLayout.setStretch(hboxPanel, 1);
116
- widgets_1.BoxLayout.setStretch(bottomPanel, 0);
117
- rootLayout.addWidget(topPanel);
118
- rootLayout.addWidget(hboxPanel);
119
- rootLayout.addWidget(bottomPanel);
120
- // initially hiding bottom panel when no elements inside
121
- this._bottomPanel.hide();
122
- this.layout = rootLayout;
123
- // Connect change listeners.
124
- this._tracker.currentChanged.connect(this._onCurrentChanged, this);
125
- this._tracker.activeChanged.connect(this._onActiveChanged, this);
126
- // Connect main layout change listener.
127
- this._dockPanel.layoutModified.connect(this._onLayoutModified, this);
128
- // Catch current changed events on the side handlers.
129
- this._leftHandler.sideBar.currentChanged.connect(this._onLayoutModified, this);
130
- this._rightHandler.sideBar.currentChanged.connect(this._onLayoutModified, this);
131
- }
132
- /**
133
- * A signal emitted when main area's active focus changes.
134
- */
135
- get activeChanged() {
136
- return this._activeChanged;
137
- }
138
- /**
139
- * The active widget in the shell's main area.
140
- */
141
- get activeWidget() {
142
- return this._tracker.activeWidget;
143
- }
144
- /**
145
- * A signal emitted when main area's current focus changes.
146
- */
147
- get currentChanged() {
148
- return this._currentChanged;
149
- }
150
- /**
151
- * The current widget in the shell's main area.
152
- */
153
- get currentWidget() {
154
- return this._tracker.currentWidget;
155
- }
156
- /**
157
- * A signal emitted when the main area's layout is modified.
158
- */
159
- get layoutModified() {
160
- return this._layoutModified;
161
- }
162
- /**
163
- * Whether the left area is collapsed.
164
- */
165
- get leftCollapsed() {
166
- return !this._leftHandler.sideBar.currentTitle;
167
- }
168
- /**
169
- * Whether the left area is collapsed.
170
- */
171
- get rightCollapsed() {
172
- return !this._rightHandler.sideBar.currentTitle;
173
- }
174
- /**
175
- * Whether JupyterLab is in presentation mode with the `jp-mod-presentationMode` CSS class.
176
- */
177
- get presentationMode() {
178
- return this.hasClass('jp-mod-presentationMode');
179
- }
180
- /**
181
- * Enable/disable presentation mode (`jp-mod-presentationMode` CSS class) with a boolean.
182
- */
183
- set presentationMode(value) {
184
- this.toggleClass('jp-mod-presentationMode', value);
185
- }
186
- /**
187
- * The main dock area's user interface mode.
188
- */
189
- get mode() {
190
- return this._dockPanel.mode;
191
- }
192
- set mode(mode) {
193
- const dock = this._dockPanel;
194
- if (mode === dock.mode) {
195
- return;
196
- }
197
- const applicationCurrentWidget = this.currentWidget;
198
- if (mode === 'single-document') {
199
- this._cachedLayout = dock.saveLayout();
200
- dock.mode = mode;
201
- // In case the active widget in the dock panel is *not* the active widget
202
- // of the application, defer to the application.
203
- if (this.currentWidget) {
204
- dock.activateWidget(this.currentWidget);
205
- }
206
- // Set the mode data attribute on the document body.
207
- document.body.setAttribute(MODE_ATTRIBUTE, mode);
208
- return;
209
- }
210
- // Cache a reference to every widget currently in the dock panel.
211
- const widgets = algorithm_1.toArray(dock.widgets());
212
- // Toggle back to multiple document mode.
213
- dock.mode = mode;
214
- // Restore the original layout.
215
- if (this._cachedLayout) {
216
- // Remove any disposed widgets in the cached layout and restore.
217
- Private.normalizeAreaConfig(dock, this._cachedLayout.main);
218
- dock.restoreLayout(this._cachedLayout);
219
- this._cachedLayout = null;
220
- }
221
- // Add any widgets created during single document mode, which have
222
- // subsequently been removed from the dock panel after the multiple document
223
- // layout has been restored. If the widget has add options cached for
224
- // it (i.e., if it has been placed with respect to another widget),
225
- // then take that into account.
226
- widgets.forEach(widget => {
227
- if (!widget.parent) {
228
- this.addToMainArea(widget, Object.assign({}, this._addOptionsCache.get(widget), { activate: false }));
229
- }
230
- });
231
- this._addOptionsCache.clear();
232
- // In case the active widget in the dock panel is *not* the active widget
233
- // of the application, defer to the application.
234
- if (applicationCurrentWidget) {
235
- dock.activateWidget(applicationCurrentWidget);
236
- }
237
- // Set the mode data attribute on the document body.
238
- document.body.setAttribute(MODE_ATTRIBUTE, mode);
239
- }
240
- /**
241
- * Promise that resolves when state is first restored, returning layout
242
- * description.
243
- */
244
- get restored() {
245
- return this._restored.promise;
246
- }
247
- /**
248
- * Activate a widget in its area.
249
- */
250
- activateById(id) {
251
- if (this._leftHandler.has(id)) {
252
- this._leftHandler.activate(id);
253
- return;
254
- }
255
- if (this._rightHandler.has(id)) {
256
- this._rightHandler.activate(id);
257
- return;
258
- }
259
- const dock = this._dockPanel;
260
- const widget = algorithm_1.find(dock.widgets(), value => value.id === id);
261
- if (widget) {
262
- dock.activateWidget(widget);
263
- }
264
- }
265
- /*
266
- * Activate the next Tab in the active TabBar.
267
- */
268
- activateNextTab() {
269
- let current = this._currentTabBar();
270
- if (!current) {
271
- return;
272
- }
273
- let ci = current.currentIndex;
274
- if (ci === -1) {
275
- return;
276
- }
277
- if (ci < current.titles.length - 1) {
278
- current.currentIndex += 1;
279
- if (current.currentTitle) {
280
- current.currentTitle.owner.activate();
281
- }
282
- return;
283
- }
284
- if (ci === current.titles.length - 1) {
285
- let nextBar = this._adjacentBar('next');
286
- if (nextBar) {
287
- nextBar.currentIndex = 0;
288
- if (nextBar.currentTitle) {
289
- nextBar.currentTitle.owner.activate();
290
- }
291
- }
292
- }
293
- }
294
- /*
295
- * Activate the previous Tab in the active TabBar.
296
- */
297
- activatePreviousTab() {
298
- let current = this._currentTabBar();
299
- if (!current) {
300
- return;
301
- }
302
- let ci = current.currentIndex;
303
- if (ci === -1) {
304
- return;
305
- }
306
- if (ci > 0) {
307
- current.currentIndex -= 1;
308
- if (current.currentTitle) {
309
- current.currentTitle.owner.activate();
310
- }
311
- return;
312
- }
313
- if (ci === 0) {
314
- let prevBar = this._adjacentBar('previous');
315
- if (prevBar) {
316
- let len = prevBar.titles.length;
317
- prevBar.currentIndex = len - 1;
318
- if (prevBar.currentTitle) {
319
- prevBar.currentTitle.owner.activate();
320
- }
321
- }
322
- }
323
- }
324
- /**
325
- * Add a widget to the left content area.
326
- *
327
- * #### Notes
328
- * Widgets must have a unique `id` property, which will be used as the DOM id.
329
- */
330
- addToLeftArea(widget, options) {
331
- if (!widget.id) {
332
- console.error('Widgets added to app shell must have unique id property.');
333
- return;
334
- }
335
- options = options || this._sideOptionsCache.get(widget) || {};
336
- this._sideOptionsCache.set(widget, options);
337
- let rank = 'rank' in options ? options.rank : DEFAULT_RANK;
338
- this._leftHandler.addWidget(widget, rank);
339
- this._onLayoutModified();
340
- }
341
- /**
342
- * Add a widget to the main content area.
343
- *
344
- * #### Notes
345
- * Widgets must have a unique `id` property, which will be used as the DOM id.
346
- * All widgets added to the main area should be disposed after removal
347
- * (disposal before removal will remove the widget automatically).
348
- *
349
- * In the options, `ref` defaults to `null`, `mode` defaults to `'tab-after'`,
350
- * and `activate` defaults to `true`.
351
- */
352
- addToMainArea(widget, options = {}) {
353
- if (!widget.id) {
354
- console.error('Widgets added to app shell must have unique id property.');
355
- return;
356
- }
357
- let dock = this._dockPanel;
358
- let ref = null;
359
- if (options.ref) {
360
- ref = algorithm_1.find(dock.widgets(), value => value.id === options.ref) || null;
361
- }
362
- let mode = options.mode || 'tab-after';
363
- dock.addWidget(widget, { mode, ref });
364
- // The dock panel doesn't account for placement information while
365
- // in single document mode, so upon rehydrating any widgets that were
366
- // added will not be in the correct place. Cache the placement information
367
- // here so that we can later rehydrate correctly.
368
- if (dock.mode === 'single-document') {
369
- this._addOptionsCache.set(widget, options);
370
- }
371
- if (options.activate !== false) {
372
- dock.activateWidget(widget);
373
- }
374
- }
375
- /**
376
- * Add a widget to the right content area.
377
- *
378
- * #### Notes
379
- * Widgets must have a unique `id` property, which will be used as the DOM id.
380
- */
381
- addToRightArea(widget, options) {
382
- if (!widget.id) {
383
- console.error('Widgets added to app shell must have unique id property.');
384
- return;
385
- }
386
- options = options || this._sideOptionsCache.get(widget) || {};
387
- this._sideOptionsCache.set(widget, options);
388
- let rank = 'rank' in options ? options.rank : DEFAULT_RANK;
389
- this._rightHandler.addWidget(widget, rank);
390
- this._onLayoutModified();
391
- }
392
- /**
393
- * Add a widget to the top content area.
394
- *
395
- * #### Notes
396
- * Widgets must have a unique `id` property, which will be used as the DOM id.
397
- */
398
- addToTopArea(widget, options = {}) {
399
- if (!widget.id) {
400
- console.error('Widgets added to app shell must have unique id property.');
401
- return;
402
- }
403
- // Temporary: widgets are added to the panel in order of insertion.
404
- this._topPanel.addWidget(widget);
405
- this._onLayoutModified();
406
- }
407
- /**
408
- * Add a widget to the bottom content area.
409
- *
410
- * #### Notes
411
- * Widgets must have a unique `id` property, which will be used as the DOM id.
412
- */
413
- addToBottomArea(widget, options = {}) {
414
- if (!widget.id) {
415
- console.error('Widgets added to app shell must have unique id property.');
416
- return;
417
- }
418
- // Temporary: widgets are added to the panel in order of insertion.
419
- this._bottomPanel.addWidget(widget);
420
- this._onLayoutModified();
421
- if (this._bottomPanel.isHidden) {
422
- this._bottomPanel.show();
423
- }
424
- }
425
- /**
426
- * Collapse the left area.
427
- */
428
- collapseLeft() {
429
- this._leftHandler.collapse();
430
- this._onLayoutModified();
431
- }
432
- /**
433
- * Collapse the right area.
434
- */
435
- collapseRight() {
436
- this._rightHandler.collapse();
437
- this._onLayoutModified();
438
- }
439
- /**
440
- * Expand the left area.
441
- *
442
- * #### Notes
443
- * This will open the most recently used tab,
444
- * or the first tab if there is no most recently used.
445
- */
446
- expandLeft() {
447
- this._leftHandler.expand();
448
- this._onLayoutModified();
449
- }
450
- /**
451
- * Expand the right area.
452
- *
453
- * #### Notes
454
- * This will open the most recently used tab,
455
- * or the first tab if there is no most recently used.
456
- */
457
- expandRight() {
458
- this._rightHandler.expand();
459
- this._onLayoutModified();
460
- }
461
- /**
462
- * Close all widgets in the main area.
463
- */
464
- closeAll() {
465
- // Make a copy of all the widget in the dock panel (using `toArray()`)
466
- // before removing them because removing them while iterating through them
467
- // modifies the underlying data of the iterator.
468
- algorithm_1.toArray(this._dockPanel.widgets()).forEach(widget => widget.close());
469
- }
470
- /**
471
- * True if the given area is empty.
472
- */
473
- isEmpty(area) {
474
- switch (area) {
475
- case 'left':
476
- return this._leftHandler.stackedPanel.widgets.length === 0;
477
- case 'main':
478
- return this._dockPanel.isEmpty;
479
- case 'top':
480
- return this._topPanel.widgets.length === 0;
481
- case 'bottom':
482
- return this._bottomPanel.widgets.length === 0;
483
- case 'right':
484
- return this._rightHandler.stackedPanel.widgets.length === 0;
485
- default:
486
- return true;
487
- }
488
- }
489
- /**
490
- * Restore the layout state for the application shell.
491
- */
492
- restoreLayout(layout) {
493
- const { mainArea, leftArea, rightArea } = layout;
494
- // Rehydrate the main area.
495
- if (mainArea) {
496
- const { currentWidget, dock, mode } = mainArea;
497
- if (dock) {
498
- this._dockPanel.restoreLayout(dock);
499
- }
500
- if (mode) {
501
- this.mode = mode;
502
- }
503
- if (currentWidget) {
504
- this.activateById(currentWidget.id);
505
- }
506
- }
507
- // Rehydrate the left area.
508
- if (leftArea) {
509
- this._leftHandler.rehydrate(leftArea);
510
- }
511
- // Rehydrate the right area.
512
- if (rightArea) {
513
- this._rightHandler.rehydrate(rightArea);
514
- }
515
- if (!this._isRestored) {
516
- // Make sure all messages in the queue are finished before notifying
517
- // any extensions that are waiting for the promise that guarantees the
518
- // application state has been restored.
519
- messaging_1.MessageLoop.flush();
520
- this._restored.resolve(layout);
521
- }
522
- }
523
- /**
524
- * Save the dehydrated state of the application shell.
525
- */
526
- saveLayout() {
527
- // If the application is in single document mode, use the cached layout if
528
- // available. Otherwise, default to querying the dock panel for layout.
529
- return {
530
- mainArea: {
531
- currentWidget: this._tracker.currentWidget,
532
- dock: this.mode === 'single-document'
533
- ? this._cachedLayout || this._dockPanel.saveLayout()
534
- : this._dockPanel.saveLayout(),
535
- mode: this._dockPanel.mode
536
- },
537
- leftArea: this._leftHandler.dehydrate(),
538
- rightArea: this._rightHandler.dehydrate()
539
- };
540
- }
541
- /**
542
- * Returns the widgets for an application area.
543
- */
544
- widgets(area) {
545
- switch (area) {
546
- case 'main':
547
- return this._dockPanel.widgets();
548
- case 'left':
549
- return algorithm_1.iter(this._leftHandler.sideBar.titles.map(t => t.owner));
550
- case 'right':
551
- return algorithm_1.iter(this._rightHandler.sideBar.titles.map(t => t.owner));
552
- case 'top':
553
- return this._topPanel.children();
554
- case 'bottom':
555
- return this._bottomPanel.children();
556
- default:
557
- throw new Error('Invalid area');
558
- }
559
- }
560
- /**
561
- * Handle `after-attach` messages for the application shell.
562
- */
563
- onAfterAttach(msg) {
564
- document.body.setAttribute(MODE_ATTRIBUTE, this.mode);
565
- }
566
- /*
567
- * Return the tab bar adjacent to the current TabBar or `null`.
568
- */
569
- _adjacentBar(direction) {
570
- const current = this._currentTabBar();
571
- if (!current) {
572
- return null;
573
- }
574
- const bars = algorithm_1.toArray(this._dockPanel.tabBars());
575
- const len = bars.length;
576
- const index = bars.indexOf(current);
577
- if (direction === 'previous') {
578
- return index > 0 ? bars[index - 1] : index === 0 ? bars[len - 1] : null;
579
- }
580
- // Otherwise, direction is 'next'.
581
- return index < len - 1
582
- ? bars[index + 1]
583
- : index === len - 1
584
- ? bars[0]
585
- : null;
586
- }
587
- /*
588
- * Return the TabBar that has the currently active Widget or null.
589
- */
590
- _currentTabBar() {
591
- const current = this._tracker.currentWidget;
592
- if (!current) {
593
- return null;
594
- }
595
- const title = current.title;
596
- const bars = this._dockPanel.tabBars();
597
- return algorithm_1.find(bars, bar => bar.titles.indexOf(title) > -1) || null;
598
- }
599
- /**
600
- * Handle a change to the dock area active widget.
601
- */
602
- _onActiveChanged(sender, args) {
603
- if (args.newValue) {
604
- args.newValue.title.className += ` ${ACTIVE_CLASS}`;
605
- }
606
- if (args.oldValue) {
607
- args.oldValue.title.className = args.oldValue.title.className.replace(ACTIVE_CLASS, '');
608
- }
609
- this._activeChanged.emit(args);
610
- }
611
- /**
612
- * Handle a change to the dock area current widget.
613
- */
614
- _onCurrentChanged(sender, args) {
615
- if (args.newValue) {
616
- args.newValue.title.className += ` ${CURRENT_CLASS}`;
617
- }
618
- if (args.oldValue) {
619
- args.oldValue.title.className = args.oldValue.title.className.replace(CURRENT_CLASS, '');
620
- }
621
- this._currentChanged.emit(args);
622
- this._onLayoutModified();
623
- }
624
- /**
625
- * Handle a change to the layout.
626
- */
627
- _onLayoutModified() {
628
- // The dock can emit layout modified signals while in transient
629
- // states (for instance, when switching from single-document to
630
- // multiple-document mode). In those states, it can be unreliable
631
- // for the signal consumers to query layout properties.
632
- // We fix this by debouncing the layout modified signal so that it
633
- // is only emitted after rearranging is done.
634
- window.clearTimeout(this._debouncer);
635
- this._debouncer = window.setTimeout(() => {
636
- this._layoutModified.emit(undefined);
637
- }, 0);
638
- }
639
- }
640
- exports.ApplicationShell = ApplicationShell;
641
- var Private;
642
- (function (Private) {
643
- /**
644
- * A less-than comparison function for side bar rank items.
645
- */
646
- function itemCmp(first, second) {
647
- return first.rank - second.rank;
648
- }
649
- Private.itemCmp = itemCmp;
650
- /**
651
- * Removes widgets that have been disposed from an area config, mutates area.
652
- */
653
- function normalizeAreaConfig(parent, area) {
654
- if (!area) {
655
- return;
656
- }
657
- if (area.type === 'tab-area') {
658
- area.widgets = area.widgets.filter(widget => !widget.isDisposed && widget.parent === parent);
659
- return;
660
- }
661
- area.children.forEach(child => {
662
- normalizeAreaConfig(parent, child);
663
- });
664
- }
665
- Private.normalizeAreaConfig = normalizeAreaConfig;
666
- /**
667
- * A class which manages a side bar and related stacked panel.
668
- */
669
- class SideBarHandler {
670
- /**
671
- * Construct a new side bar handler.
672
- */
673
- constructor(side) {
674
- this._items = new Array();
675
- this._side = side;
676
- this._sideBar = new widgets_1.TabBar({
677
- insertBehavior: 'none',
678
- removeBehavior: 'none',
679
- allowDeselect: true
680
- });
681
- this._stackedPanel = new widgets_1.StackedPanel();
682
- this._sideBar.hide();
683
- this._stackedPanel.hide();
684
- this._lastCurrent = null;
685
- this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
686
- this._sideBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
687
- this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
688
- }
689
- /**
690
- * Get the tab bar managed by the handler.
691
- */
692
- get sideBar() {
693
- return this._sideBar;
694
- }
695
- /**
696
- * Get the stacked panel managed by the handler
697
- */
698
- get stackedPanel() {
699
- return this._stackedPanel;
700
- }
701
- /**
702
- * Expand the sidebar.
703
- *
704
- * #### Notes
705
- * This will open the most recently used tab, or the first tab
706
- * if there is no most recently used.
707
- */
708
- expand() {
709
- const previous = this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
710
- if (previous) {
711
- this.activate(previous.id);
712
- }
713
- }
714
- /**
715
- * Activate a widget residing in the side bar by ID.
716
- *
717
- * @param id - The widget's unique ID.
718
- */
719
- activate(id) {
720
- let widget = this._findWidgetByID(id);
721
- if (widget) {
722
- this._sideBar.currentTitle = widget.title;
723
- widget.activate();
724
- }
725
- }
726
- /**
727
- * Test whether the sidebar has the given widget by id.
728
- */
729
- has(id) {
730
- return this._findWidgetByID(id) !== null;
731
- }
732
- /**
733
- * Collapse the sidebar so no items are expanded.
734
- */
735
- collapse() {
736
- this._sideBar.currentTitle = null;
737
- }
738
- /**
739
- * Add a widget and its title to the stacked panel and side bar.
740
- *
741
- * If the widget is already added, it will be moved.
742
- */
743
- addWidget(widget, rank) {
744
- widget.parent = null;
745
- widget.hide();
746
- let item = { widget, rank };
747
- let index = this._findInsertIndex(item);
748
- algorithm_1.ArrayExt.insert(this._items, index, item);
749
- this._stackedPanel.insertWidget(index, widget);
750
- const title = this._sideBar.insertTab(index, widget.title);
751
- // Store the parent id in the title dataset
752
- // in order to dispatch click events to the right widget.
753
- title.dataset = { id: widget.id };
754
- this._refreshVisibility();
755
- }
756
- /**
757
- * Dehydrate the side bar data.
758
- */
759
- dehydrate() {
760
- let collapsed = this._sideBar.currentTitle === null;
761
- let widgets = algorithm_1.toArray(this._stackedPanel.widgets);
762
- let currentWidget = widgets[this._sideBar.currentIndex];
763
- return { collapsed, currentWidget, widgets };
764
- }
765
- /**
766
- * Rehydrate the side bar.
767
- */
768
- rehydrate(data) {
769
- if (data.currentWidget) {
770
- this.activate(data.currentWidget.id);
771
- }
772
- else if (data.collapsed) {
773
- this.collapse();
774
- }
775
- }
776
- /**
777
- * Find the insertion index for a rank item.
778
- */
779
- _findInsertIndex(item) {
780
- return algorithm_1.ArrayExt.upperBound(this._items, item, Private.itemCmp);
781
- }
782
- /**
783
- * Find the index of the item with the given widget, or `-1`.
784
- */
785
- _findWidgetIndex(widget) {
786
- return algorithm_1.ArrayExt.findFirstIndex(this._items, i => i.widget === widget);
787
- }
788
- /**
789
- * Find the widget which owns the given title, or `null`.
790
- */
791
- _findWidgetByTitle(title) {
792
- let item = algorithm_1.find(this._items, value => value.widget.title === title);
793
- return item ? item.widget : null;
794
- }
795
- /**
796
- * Find the widget with the given id, or `null`.
797
- */
798
- _findWidgetByID(id) {
799
- let item = algorithm_1.find(this._items, value => value.widget.id === id);
800
- return item ? item.widget : null;
801
- }
802
- /**
803
- * Refresh the visibility of the side bar and stacked panel.
804
- */
805
- _refreshVisibility() {
806
- this._sideBar.setHidden(this._sideBar.titles.length === 0);
807
- this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
808
- }
809
- /**
810
- * Handle the `currentChanged` signal from the sidebar.
811
- */
812
- _onCurrentChanged(sender, args) {
813
- const oldWidget = args.previousTitle
814
- ? this._findWidgetByTitle(args.previousTitle)
815
- : null;
816
- const newWidget = args.currentTitle
817
- ? this._findWidgetByTitle(args.currentTitle)
818
- : null;
819
- if (oldWidget) {
820
- oldWidget.hide();
821
- }
822
- if (newWidget) {
823
- newWidget.show();
824
- }
825
- this._lastCurrent = newWidget || oldWidget;
826
- if (newWidget) {
827
- const id = newWidget.id;
828
- document.body.setAttribute(`data-${this._side}-sidebar-widget`, id);
829
- }
830
- else {
831
- document.body.removeAttribute(`data-${this._side}-sidebar-widget`);
832
- }
833
- this._refreshVisibility();
834
- }
835
- /**
836
- * Handle a `tabActivateRequest` signal from the sidebar.
837
- */
838
- _onTabActivateRequested(sender, args) {
839
- args.title.owner.activate();
840
- }
841
- /*
842
- * Handle the `widgetRemoved` signal from the stacked panel.
843
- */
844
- _onWidgetRemoved(sender, widget) {
845
- if (widget === this._lastCurrent) {
846
- this._lastCurrent = null;
847
- }
848
- algorithm_1.ArrayExt.removeAt(this._items, this._findWidgetIndex(widget));
849
- this._sideBar.removeTab(widget.title);
850
- this._refreshVisibility();
851
- }
852
- }
853
- Private.SideBarHandler = SideBarHandler;
854
- })(Private || (Private = {}));
1
+ "use strict";
2
+ // Copyright (c) Jupyter Development Team.
3
+ // Distributed under the terms of the Modified BSD License.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ const algorithm_1 = require("@phosphor/algorithm");
6
+ const coreutils_1 = require("@phosphor/coreutils");
7
+ const messaging_1 = require("@phosphor/messaging");
8
+ const signaling_1 = require("@phosphor/signaling");
9
+ const widgets_1 = require("@phosphor/widgets");
10
+ /**
11
+ * The class name added to AppShell instances.
12
+ */
13
+ const APPLICATION_SHELL_CLASS = 'jp-ApplicationShell';
14
+ /**
15
+ * The class name added to side bar instances.
16
+ */
17
+ const SIDEBAR_CLASS = 'jp-SideBar';
18
+ /**
19
+ * The class name added to the current widget's title.
20
+ */
21
+ const CURRENT_CLASS = 'jp-mod-current';
22
+ /**
23
+ * The class name added to the active widget's title.
24
+ */
25
+ const ACTIVE_CLASS = 'jp-mod-active';
26
+ /**
27
+ * The default rank of items added to a sidebar.
28
+ */
29
+ const DEFAULT_RANK = 500;
30
+ /**
31
+ * The data attribute added to the document body indicating shell's mode.
32
+ */
33
+ const MODE_ATTRIBUTE = 'data-shell-mode';
34
+ const ACTIVITY_CLASS = 'jp-Activity';
35
+ /**
36
+ * The application shell for JupyterLab.
37
+ */
38
+ class ApplicationShell extends widgets_1.Widget {
39
+ /**
40
+ * Construct a new application shell.
41
+ */
42
+ constructor() {
43
+ super();
44
+ /**
45
+ * A message hook for child add/remove messages on the main area dock panel.
46
+ */
47
+ this._dockChildHook = (handler, msg) => {
48
+ switch (msg.type) {
49
+ case 'child-added':
50
+ msg.child.addClass(ACTIVITY_CLASS);
51
+ this._tracker.add(msg.child);
52
+ break;
53
+ case 'child-removed':
54
+ msg.child.removeClass(ACTIVITY_CLASS);
55
+ this._tracker.remove(msg.child);
56
+ break;
57
+ default:
58
+ break;
59
+ }
60
+ return true;
61
+ };
62
+ this._activeChanged = new signaling_1.Signal(this);
63
+ this._cachedLayout = null;
64
+ this._currentChanged = new signaling_1.Signal(this);
65
+ this._isRestored = false;
66
+ this._layoutModified = new signaling_1.Signal(this);
67
+ this._restored = new coreutils_1.PromiseDelegate();
68
+ this._tracker = new widgets_1.FocusTracker();
69
+ this._debouncer = 0;
70
+ this._addOptionsCache = new Map();
71
+ this._sideOptionsCache = new Map();
72
+ this.addClass(APPLICATION_SHELL_CLASS);
73
+ this.id = 'main';
74
+ let bottomPanel = (this._bottomPanel = new widgets_1.BoxPanel());
75
+ let topPanel = (this._topPanel = new widgets_1.Panel());
76
+ let hboxPanel = new widgets_1.BoxPanel();
77
+ let dockPanel = (this._dockPanel = new widgets_1.DockPanel());
78
+ messaging_1.MessageLoop.installMessageHook(dockPanel, this._dockChildHook);
79
+ let hsplitPanel = new widgets_1.SplitPanel();
80
+ let leftHandler = (this._leftHandler = new Private.SideBarHandler('left'));
81
+ let rightHandler = (this._rightHandler = new Private.SideBarHandler('right'));
82
+ let rootLayout = new widgets_1.BoxLayout();
83
+ bottomPanel.id = 'jp-bottom-panel';
84
+ topPanel.id = 'jp-top-panel';
85
+ hboxPanel.id = 'jp-main-content-panel';
86
+ dockPanel.id = 'jp-main-dock-panel';
87
+ hsplitPanel.id = 'jp-main-split-panel';
88
+ leftHandler.sideBar.addClass(SIDEBAR_CLASS);
89
+ leftHandler.sideBar.addClass('jp-mod-left');
90
+ leftHandler.stackedPanel.id = 'jp-left-stack';
91
+ rightHandler.sideBar.addClass(SIDEBAR_CLASS);
92
+ rightHandler.sideBar.addClass('jp-mod-right');
93
+ rightHandler.stackedPanel.id = 'jp-right-stack';
94
+ bottomPanel.direction = 'bottom-to-top';
95
+ hboxPanel.spacing = 0;
96
+ dockPanel.spacing = 5;
97
+ hsplitPanel.spacing = 1;
98
+ hboxPanel.direction = 'left-to-right';
99
+ hsplitPanel.orientation = 'horizontal';
100
+ widgets_1.SplitPanel.setStretch(leftHandler.stackedPanel, 0);
101
+ widgets_1.SplitPanel.setStretch(dockPanel, 1);
102
+ widgets_1.SplitPanel.setStretch(rightHandler.stackedPanel, 0);
103
+ widgets_1.BoxPanel.setStretch(leftHandler.sideBar, 0);
104
+ widgets_1.BoxPanel.setStretch(hsplitPanel, 1);
105
+ widgets_1.BoxPanel.setStretch(rightHandler.sideBar, 0);
106
+ hsplitPanel.addWidget(leftHandler.stackedPanel);
107
+ hsplitPanel.addWidget(dockPanel);
108
+ hsplitPanel.addWidget(rightHandler.stackedPanel);
109
+ hboxPanel.addWidget(leftHandler.sideBar);
110
+ hboxPanel.addWidget(hsplitPanel);
111
+ hboxPanel.addWidget(rightHandler.sideBar);
112
+ rootLayout.direction = 'top-to-bottom';
113
+ rootLayout.spacing = 0; // TODO make this configurable?
114
+ // Use relative sizing to set the width of the side panels.
115
+ // This will still respect the min-size of children widget in the stacked panel.
116
+ hsplitPanel.setRelativeSizes([1, 2.5, 1]);
117
+ widgets_1.BoxLayout.setStretch(topPanel, 0);
118
+ widgets_1.BoxLayout.setStretch(hboxPanel, 1);
119
+ widgets_1.BoxLayout.setStretch(bottomPanel, 0);
120
+ rootLayout.addWidget(topPanel);
121
+ rootLayout.addWidget(hboxPanel);
122
+ rootLayout.addWidget(bottomPanel);
123
+ // initially hiding bottom panel when no elements inside
124
+ this._bottomPanel.hide();
125
+ this.layout = rootLayout;
126
+ // Connect change listeners.
127
+ this._tracker.currentChanged.connect(this._onCurrentChanged, this);
128
+ this._tracker.activeChanged.connect(this._onActiveChanged, this);
129
+ // Connect main layout change listener.
130
+ this._dockPanel.layoutModified.connect(this._onLayoutModified, this);
131
+ // Catch current changed events on the side handlers.
132
+ this._leftHandler.sideBar.currentChanged.connect(this._onLayoutModified, this);
133
+ this._rightHandler.sideBar.currentChanged.connect(this._onLayoutModified, this);
134
+ }
135
+ /**
136
+ * A signal emitted when main area's active focus changes.
137
+ */
138
+ get activeChanged() {
139
+ return this._activeChanged;
140
+ }
141
+ /**
142
+ * The active widget in the shell's main area.
143
+ */
144
+ get activeWidget() {
145
+ return this._tracker.activeWidget;
146
+ }
147
+ /**
148
+ * A signal emitted when main area's current focus changes.
149
+ */
150
+ get currentChanged() {
151
+ return this._currentChanged;
152
+ }
153
+ /**
154
+ * The current widget in the shell's main area.
155
+ */
156
+ get currentWidget() {
157
+ return this._tracker.currentWidget;
158
+ }
159
+ /**
160
+ * A signal emitted when the main area's layout is modified.
161
+ */
162
+ get layoutModified() {
163
+ return this._layoutModified;
164
+ }
165
+ /**
166
+ * Whether the left area is collapsed.
167
+ */
168
+ get leftCollapsed() {
169
+ return !this._leftHandler.sideBar.currentTitle;
170
+ }
171
+ /**
172
+ * Whether the left area is collapsed.
173
+ */
174
+ get rightCollapsed() {
175
+ return !this._rightHandler.sideBar.currentTitle;
176
+ }
177
+ /**
178
+ * Whether JupyterLab is in presentation mode with the `jp-mod-presentationMode` CSS class.
179
+ */
180
+ get presentationMode() {
181
+ return this.hasClass('jp-mod-presentationMode');
182
+ }
183
+ /**
184
+ * Enable/disable presentation mode (`jp-mod-presentationMode` CSS class) with a boolean.
185
+ */
186
+ set presentationMode(value) {
187
+ this.toggleClass('jp-mod-presentationMode', value);
188
+ }
189
+ /**
190
+ * The main dock area's user interface mode.
191
+ */
192
+ get mode() {
193
+ return this._dockPanel.mode;
194
+ }
195
+ set mode(mode) {
196
+ const dock = this._dockPanel;
197
+ if (mode === dock.mode) {
198
+ return;
199
+ }
200
+ const applicationCurrentWidget = this.currentWidget;
201
+ if (mode === 'single-document') {
202
+ this._cachedLayout = dock.saveLayout();
203
+ dock.mode = mode;
204
+ // In case the active widget in the dock panel is *not* the active widget
205
+ // of the application, defer to the application.
206
+ if (this.currentWidget) {
207
+ dock.activateWidget(this.currentWidget);
208
+ }
209
+ // Set the mode data attribute on the document body.
210
+ document.body.setAttribute(MODE_ATTRIBUTE, mode);
211
+ return;
212
+ }
213
+ // Cache a reference to every widget currently in the dock panel.
214
+ const widgets = algorithm_1.toArray(dock.widgets());
215
+ // Toggle back to multiple document mode.
216
+ dock.mode = mode;
217
+ // Restore the original layout.
218
+ if (this._cachedLayout) {
219
+ // Remove any disposed widgets in the cached layout and restore.
220
+ Private.normalizeAreaConfig(dock, this._cachedLayout.main);
221
+ dock.restoreLayout(this._cachedLayout);
222
+ this._cachedLayout = null;
223
+ }
224
+ // Add any widgets created during single document mode, which have
225
+ // subsequently been removed from the dock panel after the multiple document
226
+ // layout has been restored. If the widget has add options cached for
227
+ // it (i.e., if it has been placed with respect to another widget),
228
+ // then take that into account.
229
+ widgets.forEach(widget => {
230
+ if (!widget.parent) {
231
+ this.addToMainArea(widget, Object.assign({}, this._addOptionsCache.get(widget), { activate: false }));
232
+ }
233
+ });
234
+ this._addOptionsCache.clear();
235
+ // In case the active widget in the dock panel is *not* the active widget
236
+ // of the application, defer to the application.
237
+ if (applicationCurrentWidget) {
238
+ dock.activateWidget(applicationCurrentWidget);
239
+ }
240
+ // Set the mode data attribute on the document body.
241
+ document.body.setAttribute(MODE_ATTRIBUTE, mode);
242
+ }
243
+ /**
244
+ * Promise that resolves when state is first restored, returning layout
245
+ * description.
246
+ */
247
+ get restored() {
248
+ return this._restored.promise;
249
+ }
250
+ /**
251
+ * Activate a widget in its area.
252
+ */
253
+ activateById(id) {
254
+ if (this._leftHandler.has(id)) {
255
+ this._leftHandler.activate(id);
256
+ return;
257
+ }
258
+ if (this._rightHandler.has(id)) {
259
+ this._rightHandler.activate(id);
260
+ return;
261
+ }
262
+ const dock = this._dockPanel;
263
+ const widget = algorithm_1.find(dock.widgets(), value => value.id === id);
264
+ if (widget) {
265
+ dock.activateWidget(widget);
266
+ }
267
+ }
268
+ /*
269
+ * Activate the next Tab in the active TabBar.
270
+ */
271
+ activateNextTab() {
272
+ let current = this._currentTabBar();
273
+ if (!current) {
274
+ return;
275
+ }
276
+ let ci = current.currentIndex;
277
+ if (ci === -1) {
278
+ return;
279
+ }
280
+ if (ci < current.titles.length - 1) {
281
+ current.currentIndex += 1;
282
+ if (current.currentTitle) {
283
+ current.currentTitle.owner.activate();
284
+ }
285
+ return;
286
+ }
287
+ if (ci === current.titles.length - 1) {
288
+ let nextBar = this._adjacentBar('next');
289
+ if (nextBar) {
290
+ nextBar.currentIndex = 0;
291
+ if (nextBar.currentTitle) {
292
+ nextBar.currentTitle.owner.activate();
293
+ }
294
+ }
295
+ }
296
+ }
297
+ /*
298
+ * Activate the previous Tab in the active TabBar.
299
+ */
300
+ activatePreviousTab() {
301
+ let current = this._currentTabBar();
302
+ if (!current) {
303
+ return;
304
+ }
305
+ let ci = current.currentIndex;
306
+ if (ci === -1) {
307
+ return;
308
+ }
309
+ if (ci > 0) {
310
+ current.currentIndex -= 1;
311
+ if (current.currentTitle) {
312
+ current.currentTitle.owner.activate();
313
+ }
314
+ return;
315
+ }
316
+ if (ci === 0) {
317
+ let prevBar = this._adjacentBar('previous');
318
+ if (prevBar) {
319
+ let len = prevBar.titles.length;
320
+ prevBar.currentIndex = len - 1;
321
+ if (prevBar.currentTitle) {
322
+ prevBar.currentTitle.owner.activate();
323
+ }
324
+ }
325
+ }
326
+ }
327
+ /**
328
+ * Add a widget to the left content area.
329
+ *
330
+ * #### Notes
331
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
332
+ */
333
+ addToLeftArea(widget, options) {
334
+ if (!widget.id) {
335
+ console.error('Widgets added to app shell must have unique id property.');
336
+ return;
337
+ }
338
+ options = options || this._sideOptionsCache.get(widget) || {};
339
+ this._sideOptionsCache.set(widget, options);
340
+ let rank = 'rank' in options ? options.rank : DEFAULT_RANK;
341
+ this._leftHandler.addWidget(widget, rank);
342
+ this._onLayoutModified();
343
+ }
344
+ /**
345
+ * Add a widget to the main content area.
346
+ *
347
+ * #### Notes
348
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
349
+ * All widgets added to the main area should be disposed after removal
350
+ * (disposal before removal will remove the widget automatically).
351
+ *
352
+ * In the options, `ref` defaults to `null`, `mode` defaults to `'tab-after'`,
353
+ * and `activate` defaults to `true`.
354
+ */
355
+ addToMainArea(widget, options = {}) {
356
+ if (!widget.id) {
357
+ console.error('Widgets added to app shell must have unique id property.');
358
+ return;
359
+ }
360
+ let dock = this._dockPanel;
361
+ let ref = null;
362
+ if (options.ref) {
363
+ ref = algorithm_1.find(dock.widgets(), value => value.id === options.ref) || null;
364
+ }
365
+ let mode = options.mode || 'tab-after';
366
+ dock.addWidget(widget, { mode, ref });
367
+ // The dock panel doesn't account for placement information while
368
+ // in single document mode, so upon rehydrating any widgets that were
369
+ // added will not be in the correct place. Cache the placement information
370
+ // here so that we can later rehydrate correctly.
371
+ if (dock.mode === 'single-document') {
372
+ this._addOptionsCache.set(widget, options);
373
+ }
374
+ if (options.activate !== false) {
375
+ dock.activateWidget(widget);
376
+ }
377
+ }
378
+ /**
379
+ * Add a widget to the right content area.
380
+ *
381
+ * #### Notes
382
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
383
+ */
384
+ addToRightArea(widget, options) {
385
+ if (!widget.id) {
386
+ console.error('Widgets added to app shell must have unique id property.');
387
+ return;
388
+ }
389
+ options = options || this._sideOptionsCache.get(widget) || {};
390
+ this._sideOptionsCache.set(widget, options);
391
+ let rank = 'rank' in options ? options.rank : DEFAULT_RANK;
392
+ this._rightHandler.addWidget(widget, rank);
393
+ this._onLayoutModified();
394
+ }
395
+ /**
396
+ * Add a widget to the top content area.
397
+ *
398
+ * #### Notes
399
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
400
+ */
401
+ addToTopArea(widget, options = {}) {
402
+ if (!widget.id) {
403
+ console.error('Widgets added to app shell must have unique id property.');
404
+ return;
405
+ }
406
+ // Temporary: widgets are added to the panel in order of insertion.
407
+ this._topPanel.addWidget(widget);
408
+ this._onLayoutModified();
409
+ }
410
+ /**
411
+ * Add a widget to the bottom content area.
412
+ *
413
+ * #### Notes
414
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
415
+ */
416
+ addToBottomArea(widget, options = {}) {
417
+ if (!widget.id) {
418
+ console.error('Widgets added to app shell must have unique id property.');
419
+ return;
420
+ }
421
+ // Temporary: widgets are added to the panel in order of insertion.
422
+ this._bottomPanel.addWidget(widget);
423
+ this._onLayoutModified();
424
+ if (this._bottomPanel.isHidden) {
425
+ this._bottomPanel.show();
426
+ }
427
+ }
428
+ /**
429
+ * Collapse the left area.
430
+ */
431
+ collapseLeft() {
432
+ this._leftHandler.collapse();
433
+ this._onLayoutModified();
434
+ }
435
+ /**
436
+ * Collapse the right area.
437
+ */
438
+ collapseRight() {
439
+ this._rightHandler.collapse();
440
+ this._onLayoutModified();
441
+ }
442
+ /**
443
+ * Expand the left area.
444
+ *
445
+ * #### Notes
446
+ * This will open the most recently used tab,
447
+ * or the first tab if there is no most recently used.
448
+ */
449
+ expandLeft() {
450
+ this._leftHandler.expand();
451
+ this._onLayoutModified();
452
+ }
453
+ /**
454
+ * Expand the right area.
455
+ *
456
+ * #### Notes
457
+ * This will open the most recently used tab,
458
+ * or the first tab if there is no most recently used.
459
+ */
460
+ expandRight() {
461
+ this._rightHandler.expand();
462
+ this._onLayoutModified();
463
+ }
464
+ /**
465
+ * Close all widgets in the main area.
466
+ */
467
+ closeAll() {
468
+ // Make a copy of all the widget in the dock panel (using `toArray()`)
469
+ // before removing them because removing them while iterating through them
470
+ // modifies the underlying data of the iterator.
471
+ algorithm_1.toArray(this._dockPanel.widgets()).forEach(widget => widget.close());
472
+ }
473
+ /**
474
+ * True if the given area is empty.
475
+ */
476
+ isEmpty(area) {
477
+ switch (area) {
478
+ case 'left':
479
+ return this._leftHandler.stackedPanel.widgets.length === 0;
480
+ case 'main':
481
+ return this._dockPanel.isEmpty;
482
+ case 'top':
483
+ return this._topPanel.widgets.length === 0;
484
+ case 'bottom':
485
+ return this._bottomPanel.widgets.length === 0;
486
+ case 'right':
487
+ return this._rightHandler.stackedPanel.widgets.length === 0;
488
+ default:
489
+ return true;
490
+ }
491
+ }
492
+ /**
493
+ * Restore the layout state for the application shell.
494
+ */
495
+ restoreLayout(layout) {
496
+ const { mainArea, leftArea, rightArea } = layout;
497
+ // Rehydrate the main area.
498
+ if (mainArea) {
499
+ const { currentWidget, dock, mode } = mainArea;
500
+ if (dock) {
501
+ this._dockPanel.restoreLayout(dock);
502
+ }
503
+ if (mode) {
504
+ this.mode = mode;
505
+ }
506
+ if (currentWidget) {
507
+ this.activateById(currentWidget.id);
508
+ }
509
+ }
510
+ // Rehydrate the left area.
511
+ if (leftArea) {
512
+ this._leftHandler.rehydrate(leftArea);
513
+ }
514
+ // Rehydrate the right area.
515
+ if (rightArea) {
516
+ this._rightHandler.rehydrate(rightArea);
517
+ }
518
+ if (!this._isRestored) {
519
+ // Make sure all messages in the queue are finished before notifying
520
+ // any extensions that are waiting for the promise that guarantees the
521
+ // application state has been restored.
522
+ messaging_1.MessageLoop.flush();
523
+ this._restored.resolve(layout);
524
+ }
525
+ }
526
+ /**
527
+ * Save the dehydrated state of the application shell.
528
+ */
529
+ saveLayout() {
530
+ // If the application is in single document mode, use the cached layout if
531
+ // available. Otherwise, default to querying the dock panel for layout.
532
+ return {
533
+ mainArea: {
534
+ currentWidget: this._tracker.currentWidget,
535
+ dock: this.mode === 'single-document'
536
+ ? this._cachedLayout || this._dockPanel.saveLayout()
537
+ : this._dockPanel.saveLayout(),
538
+ mode: this._dockPanel.mode
539
+ },
540
+ leftArea: this._leftHandler.dehydrate(),
541
+ rightArea: this._rightHandler.dehydrate()
542
+ };
543
+ }
544
+ /**
545
+ * Returns the widgets for an application area.
546
+ */
547
+ widgets(area) {
548
+ switch (area) {
549
+ case 'main':
550
+ return this._dockPanel.widgets();
551
+ case 'left':
552
+ return algorithm_1.iter(this._leftHandler.sideBar.titles.map(t => t.owner));
553
+ case 'right':
554
+ return algorithm_1.iter(this._rightHandler.sideBar.titles.map(t => t.owner));
555
+ case 'top':
556
+ return this._topPanel.children();
557
+ case 'bottom':
558
+ return this._bottomPanel.children();
559
+ default:
560
+ throw new Error('Invalid area');
561
+ }
562
+ }
563
+ /**
564
+ * Handle `after-attach` messages for the application shell.
565
+ */
566
+ onAfterAttach(msg) {
567
+ document.body.setAttribute(MODE_ATTRIBUTE, this.mode);
568
+ }
569
+ /*
570
+ * Return the tab bar adjacent to the current TabBar or `null`.
571
+ */
572
+ _adjacentBar(direction) {
573
+ const current = this._currentTabBar();
574
+ if (!current) {
575
+ return null;
576
+ }
577
+ const bars = algorithm_1.toArray(this._dockPanel.tabBars());
578
+ const len = bars.length;
579
+ const index = bars.indexOf(current);
580
+ if (direction === 'previous') {
581
+ return index > 0 ? bars[index - 1] : index === 0 ? bars[len - 1] : null;
582
+ }
583
+ // Otherwise, direction is 'next'.
584
+ return index < len - 1
585
+ ? bars[index + 1]
586
+ : index === len - 1
587
+ ? bars[0]
588
+ : null;
589
+ }
590
+ /*
591
+ * Return the TabBar that has the currently active Widget or null.
592
+ */
593
+ _currentTabBar() {
594
+ const current = this._tracker.currentWidget;
595
+ if (!current) {
596
+ return null;
597
+ }
598
+ const title = current.title;
599
+ const bars = this._dockPanel.tabBars();
600
+ return algorithm_1.find(bars, bar => bar.titles.indexOf(title) > -1) || null;
601
+ }
602
+ /**
603
+ * Handle a change to the dock area active widget.
604
+ */
605
+ _onActiveChanged(sender, args) {
606
+ if (args.newValue) {
607
+ args.newValue.title.className += ` ${ACTIVE_CLASS}`;
608
+ }
609
+ if (args.oldValue) {
610
+ args.oldValue.title.className = args.oldValue.title.className.replace(ACTIVE_CLASS, '');
611
+ }
612
+ this._activeChanged.emit(args);
613
+ }
614
+ /**
615
+ * Handle a change to the dock area current widget.
616
+ */
617
+ _onCurrentChanged(sender, args) {
618
+ if (args.newValue) {
619
+ args.newValue.title.className += ` ${CURRENT_CLASS}`;
620
+ }
621
+ if (args.oldValue) {
622
+ args.oldValue.title.className = args.oldValue.title.className.replace(CURRENT_CLASS, '');
623
+ }
624
+ this._currentChanged.emit(args);
625
+ this._onLayoutModified();
626
+ }
627
+ /**
628
+ * Handle a change to the layout.
629
+ */
630
+ _onLayoutModified() {
631
+ // The dock can emit layout modified signals while in transient
632
+ // states (for instance, when switching from single-document to
633
+ // multiple-document mode). In those states, it can be unreliable
634
+ // for the signal consumers to query layout properties.
635
+ // We fix this by debouncing the layout modified signal so that it
636
+ // is only emitted after rearranging is done.
637
+ window.clearTimeout(this._debouncer);
638
+ this._debouncer = window.setTimeout(() => {
639
+ this._layoutModified.emit(undefined);
640
+ }, 0);
641
+ }
642
+ }
643
+ exports.ApplicationShell = ApplicationShell;
644
+ var Private;
645
+ (function (Private) {
646
+ /**
647
+ * A less-than comparison function for side bar rank items.
648
+ */
649
+ function itemCmp(first, second) {
650
+ return first.rank - second.rank;
651
+ }
652
+ Private.itemCmp = itemCmp;
653
+ /**
654
+ * Removes widgets that have been disposed from an area config, mutates area.
655
+ */
656
+ function normalizeAreaConfig(parent, area) {
657
+ if (!area) {
658
+ return;
659
+ }
660
+ if (area.type === 'tab-area') {
661
+ area.widgets = area.widgets.filter(widget => !widget.isDisposed && widget.parent === parent);
662
+ return;
663
+ }
664
+ area.children.forEach(child => {
665
+ normalizeAreaConfig(parent, child);
666
+ });
667
+ }
668
+ Private.normalizeAreaConfig = normalizeAreaConfig;
669
+ /**
670
+ * A class which manages a side bar and related stacked panel.
671
+ */
672
+ class SideBarHandler {
673
+ /**
674
+ * Construct a new side bar handler.
675
+ */
676
+ constructor(side) {
677
+ this._items = new Array();
678
+ this._side = side;
679
+ this._sideBar = new widgets_1.TabBar({
680
+ insertBehavior: 'none',
681
+ removeBehavior: 'none',
682
+ allowDeselect: true
683
+ });
684
+ this._stackedPanel = new widgets_1.StackedPanel();
685
+ this._sideBar.hide();
686
+ this._stackedPanel.hide();
687
+ this._lastCurrent = null;
688
+ this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
689
+ this._sideBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
690
+ this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
691
+ }
692
+ /**
693
+ * Get the tab bar managed by the handler.
694
+ */
695
+ get sideBar() {
696
+ return this._sideBar;
697
+ }
698
+ /**
699
+ * Get the stacked panel managed by the handler
700
+ */
701
+ get stackedPanel() {
702
+ return this._stackedPanel;
703
+ }
704
+ /**
705
+ * Expand the sidebar.
706
+ *
707
+ * #### Notes
708
+ * This will open the most recently used tab, or the first tab
709
+ * if there is no most recently used.
710
+ */
711
+ expand() {
712
+ const previous = this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
713
+ if (previous) {
714
+ this.activate(previous.id);
715
+ }
716
+ }
717
+ /**
718
+ * Activate a widget residing in the side bar by ID.
719
+ *
720
+ * @param id - The widget's unique ID.
721
+ */
722
+ activate(id) {
723
+ let widget = this._findWidgetByID(id);
724
+ if (widget) {
725
+ this._sideBar.currentTitle = widget.title;
726
+ widget.activate();
727
+ }
728
+ }
729
+ /**
730
+ * Test whether the sidebar has the given widget by id.
731
+ */
732
+ has(id) {
733
+ return this._findWidgetByID(id) !== null;
734
+ }
735
+ /**
736
+ * Collapse the sidebar so no items are expanded.
737
+ */
738
+ collapse() {
739
+ this._sideBar.currentTitle = null;
740
+ }
741
+ /**
742
+ * Add a widget and its title to the stacked panel and side bar.
743
+ *
744
+ * If the widget is already added, it will be moved.
745
+ */
746
+ addWidget(widget, rank) {
747
+ widget.parent = null;
748
+ widget.hide();
749
+ let item = { widget, rank };
750
+ let index = this._findInsertIndex(item);
751
+ algorithm_1.ArrayExt.insert(this._items, index, item);
752
+ this._stackedPanel.insertWidget(index, widget);
753
+ const title = this._sideBar.insertTab(index, widget.title);
754
+ // Store the parent id in the title dataset
755
+ // in order to dispatch click events to the right widget.
756
+ title.dataset = { id: widget.id };
757
+ this._refreshVisibility();
758
+ }
759
+ /**
760
+ * Dehydrate the side bar data.
761
+ */
762
+ dehydrate() {
763
+ let collapsed = this._sideBar.currentTitle === null;
764
+ let widgets = algorithm_1.toArray(this._stackedPanel.widgets);
765
+ let currentWidget = widgets[this._sideBar.currentIndex];
766
+ return { collapsed, currentWidget, widgets };
767
+ }
768
+ /**
769
+ * Rehydrate the side bar.
770
+ */
771
+ rehydrate(data) {
772
+ if (data.currentWidget) {
773
+ this.activate(data.currentWidget.id);
774
+ }
775
+ else if (data.collapsed) {
776
+ this.collapse();
777
+ }
778
+ }
779
+ /**
780
+ * Find the insertion index for a rank item.
781
+ */
782
+ _findInsertIndex(item) {
783
+ return algorithm_1.ArrayExt.upperBound(this._items, item, Private.itemCmp);
784
+ }
785
+ /**
786
+ * Find the index of the item with the given widget, or `-1`.
787
+ */
788
+ _findWidgetIndex(widget) {
789
+ return algorithm_1.ArrayExt.findFirstIndex(this._items, i => i.widget === widget);
790
+ }
791
+ /**
792
+ * Find the widget which owns the given title, or `null`.
793
+ */
794
+ _findWidgetByTitle(title) {
795
+ let item = algorithm_1.find(this._items, value => value.widget.title === title);
796
+ return item ? item.widget : null;
797
+ }
798
+ /**
799
+ * Find the widget with the given id, or `null`.
800
+ */
801
+ _findWidgetByID(id) {
802
+ let item = algorithm_1.find(this._items, value => value.widget.id === id);
803
+ return item ? item.widget : null;
804
+ }
805
+ /**
806
+ * Refresh the visibility of the side bar and stacked panel.
807
+ */
808
+ _refreshVisibility() {
809
+ this._sideBar.setHidden(this._sideBar.titles.length === 0);
810
+ this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
811
+ }
812
+ /**
813
+ * Handle the `currentChanged` signal from the sidebar.
814
+ */
815
+ _onCurrentChanged(sender, args) {
816
+ const oldWidget = args.previousTitle
817
+ ? this._findWidgetByTitle(args.previousTitle)
818
+ : null;
819
+ const newWidget = args.currentTitle
820
+ ? this._findWidgetByTitle(args.currentTitle)
821
+ : null;
822
+ if (oldWidget) {
823
+ oldWidget.hide();
824
+ }
825
+ if (newWidget) {
826
+ newWidget.show();
827
+ }
828
+ this._lastCurrent = newWidget || oldWidget;
829
+ if (newWidget) {
830
+ const id = newWidget.id;
831
+ document.body.setAttribute(`data-${this._side}-sidebar-widget`, id);
832
+ }
833
+ else {
834
+ document.body.removeAttribute(`data-${this._side}-sidebar-widget`);
835
+ }
836
+ this._refreshVisibility();
837
+ }
838
+ /**
839
+ * Handle a `tabActivateRequest` signal from the sidebar.
840
+ */
841
+ _onTabActivateRequested(sender, args) {
842
+ args.title.owner.activate();
843
+ }
844
+ /*
845
+ * Handle the `widgetRemoved` signal from the stacked panel.
846
+ */
847
+ _onWidgetRemoved(sender, widget) {
848
+ if (widget === this._lastCurrent) {
849
+ this._lastCurrent = null;
850
+ }
851
+ algorithm_1.ArrayExt.removeAt(this._items, this._findWidgetIndex(widget));
852
+ this._sideBar.removeTab(widget.title);
853
+ this._refreshVisibility();
854
+ }
855
+ }
856
+ Private.SideBarHandler = SideBarHandler;
857
+ })(Private || (Private = {}));