@jupyterlab/application 4.0.0-alpha.9 → 4.0.0-beta.0

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/src/shell.ts ADDED
@@ -0,0 +1,2285 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { DocumentRegistry, DocumentWidget } from '@jupyterlab/docregistry';
5
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
6
+ import {
7
+ classes,
8
+ DockPanelSvg,
9
+ LabIcon,
10
+ TabBarSvg,
11
+ tabIcon,
12
+ TabPanelSvg
13
+ } from '@jupyterlab/ui-components';
14
+ import { ArrayExt, find, map } from '@lumino/algorithm';
15
+ import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils';
16
+ import { IMessageHandler, Message, MessageLoop } from '@lumino/messaging';
17
+ import { Debouncer } from '@lumino/polling';
18
+ import { ISignal, Signal } from '@lumino/signaling';
19
+ import {
20
+ BoxLayout,
21
+ BoxPanel,
22
+ DockLayout,
23
+ DockPanel,
24
+ FocusTracker,
25
+ Panel,
26
+ SplitPanel,
27
+ StackedPanel,
28
+ TabBar,
29
+ TabPanel,
30
+ Title,
31
+ Widget
32
+ } from '@lumino/widgets';
33
+ import { JupyterFrontEnd } from './frontend';
34
+ import { LayoutRestorer } from './layoutrestorer';
35
+
36
+ /**
37
+ * The class name added to AppShell instances.
38
+ */
39
+ const APPLICATION_SHELL_CLASS = 'jp-LabShell';
40
+
41
+ /**
42
+ * The class name added to side bar instances.
43
+ */
44
+ const SIDEBAR_CLASS = 'jp-SideBar';
45
+
46
+ /**
47
+ * The class name added to the current widget's title.
48
+ */
49
+ const CURRENT_CLASS = 'jp-mod-current';
50
+
51
+ /**
52
+ * The class name added to the active widget's title.
53
+ */
54
+ const ACTIVE_CLASS = 'jp-mod-active';
55
+
56
+ /**
57
+ * The default rank of items added to a sidebar.
58
+ */
59
+ const DEFAULT_RANK = 900;
60
+
61
+ const ACTIVITY_CLASS = 'jp-Activity';
62
+
63
+ /**
64
+ * The JupyterLab application shell token.
65
+ */
66
+ export const ILabShell = new Token<ILabShell>(
67
+ '@jupyterlab/application:ILabShell'
68
+ );
69
+
70
+ /**
71
+ * The JupyterLab application shell interface.
72
+ */
73
+ export interface ILabShell extends LabShell {}
74
+
75
+ /**
76
+ * The namespace for `ILabShell` type information.
77
+ */
78
+ export namespace ILabShell {
79
+ /**
80
+ * The areas of the application shell where widgets can reside.
81
+ */
82
+ export type Area =
83
+ | 'main'
84
+ | 'header'
85
+ | 'top'
86
+ | 'menu'
87
+ | 'left'
88
+ | 'right'
89
+ | 'bottom'
90
+ | 'down';
91
+
92
+ /**
93
+ * The restorable description of an area within the main dock panel.
94
+ */
95
+ export type AreaConfig = DockLayout.AreaConfig;
96
+
97
+ /**
98
+ * An options object for creating a lab shell object.
99
+ */
100
+ export type IOptions = {
101
+ /**
102
+ * Whether the layout should wait to be restored before adding widgets or not.
103
+ *
104
+ * #### Notes
105
+ * It defaults to true
106
+ */
107
+ waitForRestore?: boolean;
108
+ };
109
+
110
+ /**
111
+ * An arguments object for the changed signals.
112
+ */
113
+ export type IChangedArgs = FocusTracker.IChangedArgs<Widget>;
114
+
115
+ export interface IConfig {
116
+ /**
117
+ * The method for hiding widgets in the dock panel.
118
+ *
119
+ * The default is `display`.
120
+ *
121
+ * Using `scale` will often increase performance as most browsers will not trigger style computation
122
+ * for the transform action.
123
+ *
124
+ * `contentVisibility` is only available in Chromium-based browsers.
125
+ */
126
+ hiddenMode: 'display' | 'scale' | 'contentVisibility';
127
+ }
128
+
129
+ /**
130
+ * Widget position
131
+ */
132
+ export interface IWidgetPosition {
133
+ /**
134
+ * Widget area
135
+ */
136
+ area?: Area;
137
+ /**
138
+ * Widget opening options
139
+ */
140
+ options?: DocumentRegistry.IOpenOptions;
141
+ }
142
+
143
+ /**
144
+ * To-be-added widget and associated position
145
+ */
146
+ export interface IDelayedWidget extends IWidgetPosition {
147
+ widget: Widget;
148
+ }
149
+
150
+ /**
151
+ * Mapping of widget type identifier and their user customized position
152
+ */
153
+ export interface IUserLayout {
154
+ /**
155
+ * Widget customized position
156
+ */
157
+ [k: string]: IWidgetPosition;
158
+ }
159
+
160
+ /**
161
+ * The args for the current path change signal.
162
+ */
163
+ export interface ICurrentPathChangedArgs {
164
+ /**
165
+ * The new value of the tree path, not including '/tree'.
166
+ */
167
+ oldValue: string;
168
+
169
+ /**
170
+ * The old value of the tree path, not including '/tree'.
171
+ */
172
+ newValue: string;
173
+ }
174
+
175
+ /**
176
+ * A description of the application's user interface layout.
177
+ */
178
+ export interface ILayout {
179
+ /**
180
+ * Indicates whether fetched session restore data was actually retrieved
181
+ * from the state database or whether it is a fresh blank slate.
182
+ *
183
+ * #### Notes
184
+ * This attribute is only relevant when the layout data is retrieved via a
185
+ * `fetch` call. If it is set when being passed into `save`, it will be
186
+ * ignored.
187
+ */
188
+ readonly fresh?: boolean;
189
+
190
+ /**
191
+ * The main area of the user interface.
192
+ */
193
+ readonly mainArea: IMainArea | null;
194
+
195
+ /**
196
+ * The down area of the user interface.
197
+ */
198
+ readonly downArea: IDownArea | null;
199
+
200
+ /**
201
+ * The left area of the user interface.
202
+ */
203
+ readonly leftArea: ISideArea | null;
204
+
205
+ /**
206
+ * The right area of the user interface.
207
+ */
208
+ readonly rightArea: ISideArea | null;
209
+
210
+ /**
211
+ * The top area of the user interface.
212
+ */
213
+ readonly topArea: ITopArea | null;
214
+
215
+ /**
216
+ * The relatives sizes of the areas of the user interface.
217
+ */
218
+ readonly relativeSizes: number[] | null;
219
+ }
220
+
221
+ /**
222
+ * The restorable description of the main application area.
223
+ */
224
+ export interface IMainArea {
225
+ /**
226
+ * The current widget that has application focus.
227
+ */
228
+ readonly currentWidget: Widget | null;
229
+
230
+ /**
231
+ * The contents of the main application dock panel.
232
+ */
233
+ readonly dock: DockLayout.ILayoutConfig | null;
234
+ }
235
+
236
+ export interface IDownArea {
237
+ /**
238
+ * The current widget that has down area focus.
239
+ */
240
+ readonly currentWidget: Widget | null;
241
+
242
+ /**
243
+ * The collection of widgets held by the panel.
244
+ */
245
+ readonly widgets: Array<Widget> | null;
246
+
247
+ /**
248
+ * Vertical relative size of the down area
249
+ *
250
+ * The main area will take the rest of the height
251
+ */
252
+ readonly size: number | null;
253
+ }
254
+
255
+ /**
256
+ * The restorable description of a sidebar in the user interface.
257
+ */
258
+ export interface ISideArea {
259
+ /**
260
+ * A flag denoting whether the sidebar has been collapsed.
261
+ */
262
+ readonly collapsed: boolean;
263
+
264
+ /**
265
+ * The current widget that has side area focus.
266
+ */
267
+ readonly currentWidget: Widget | null;
268
+
269
+ /**
270
+ * A flag denoting whether the side tab bar is visible.
271
+ */
272
+ readonly visible: boolean;
273
+
274
+ /**
275
+ * The collection of widgets held by the sidebar.
276
+ */
277
+ readonly widgets: Array<Widget> | null;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * The restorable description of the top area in the user interface.
283
+ */
284
+ export interface ITopArea {
285
+ /**
286
+ * Top area visibility in simple mode.
287
+ */
288
+ readonly simpleVisibility: boolean;
289
+ }
290
+
291
+ /**
292
+ * The application shell for JupyterLab.
293
+ */
294
+ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
295
+ /**
296
+ * Construct a new application shell.
297
+ */
298
+ constructor(options?: ILabShell.IOptions) {
299
+ super();
300
+ this.addClass(APPLICATION_SHELL_CLASS);
301
+ this.id = 'main';
302
+ if (options?.waitForRestore === false) {
303
+ this._userLayout = { 'multiple-document': {}, 'single-document': {} };
304
+ }
305
+
306
+ // Skip Links
307
+ const skipLinkWidget = (this._skipLinkWidget = new Private.SkipLinkWidget(
308
+ this
309
+ ));
310
+ this._skipLinkWidget.show();
311
+ // Wrap the skip widget to customize its position and size
312
+ const skipLinkWrapper = new Panel();
313
+ skipLinkWrapper.addClass('jp-skiplink-wrapper');
314
+ skipLinkWrapper.addWidget(skipLinkWidget);
315
+
316
+ const headerPanel = (this._headerPanel = new BoxPanel());
317
+ const menuHandler = (this._menuHandler = new Private.PanelHandler());
318
+ menuHandler.panel.node.setAttribute('role', 'navigation');
319
+ const topHandler = (this._topHandler = new Private.PanelHandler());
320
+ topHandler.panel.node.setAttribute('role', 'banner');
321
+ const bottomPanel = (this._bottomPanel = new BoxPanel());
322
+ bottomPanel.node.setAttribute('role', 'contentinfo');
323
+ const hboxPanel = new BoxPanel();
324
+ const vsplitPanel = (this._vsplitPanel =
325
+ new Private.RestorableSplitPanel());
326
+ const dockPanel = (this._dockPanel = new DockPanelSvg({
327
+ hiddenMode: Widget.HiddenMode.Display
328
+ }));
329
+ MessageLoop.installMessageHook(dockPanel, this._dockChildHook);
330
+
331
+ const hsplitPanel = (this._hsplitPanel =
332
+ new Private.RestorableSplitPanel());
333
+ const downPanel = (this._downPanel = new TabPanelSvg({
334
+ tabsMovable: true
335
+ }));
336
+ const leftHandler = (this._leftHandler = new Private.SideBarHandler());
337
+ const rightHandler = (this._rightHandler = new Private.SideBarHandler());
338
+ const rootLayout = new BoxLayout();
339
+
340
+ headerPanel.id = 'jp-header-panel';
341
+ menuHandler.panel.id = 'jp-menu-panel';
342
+ topHandler.panel.id = 'jp-top-panel';
343
+ bottomPanel.id = 'jp-bottom-panel';
344
+ hboxPanel.id = 'jp-main-content-panel';
345
+ vsplitPanel.id = 'jp-main-vsplit-panel';
346
+ dockPanel.id = 'jp-main-dock-panel';
347
+ hsplitPanel.id = 'jp-main-split-panel';
348
+ downPanel.id = 'jp-down-stack';
349
+
350
+ leftHandler.sideBar.addClass(SIDEBAR_CLASS);
351
+ leftHandler.sideBar.addClass('jp-mod-left');
352
+ leftHandler.sideBar.node.setAttribute('role', 'complementary');
353
+ leftHandler.stackedPanel.id = 'jp-left-stack';
354
+
355
+ rightHandler.sideBar.addClass(SIDEBAR_CLASS);
356
+ rightHandler.sideBar.addClass('jp-mod-right');
357
+ rightHandler.sideBar.node.setAttribute('role', 'complementary');
358
+ rightHandler.stackedPanel.id = 'jp-right-stack';
359
+
360
+ dockPanel.node.setAttribute('role', 'main');
361
+
362
+ hboxPanel.spacing = 0;
363
+ vsplitPanel.spacing = 1;
364
+ dockPanel.spacing = 5;
365
+ hsplitPanel.spacing = 1;
366
+
367
+ headerPanel.direction = 'top-to-bottom';
368
+ vsplitPanel.orientation = 'vertical';
369
+ hboxPanel.direction = 'left-to-right';
370
+ hsplitPanel.orientation = 'horizontal';
371
+ bottomPanel.direction = 'bottom-to-top';
372
+
373
+ SplitPanel.setStretch(leftHandler.stackedPanel, 0);
374
+ SplitPanel.setStretch(downPanel, 0);
375
+ SplitPanel.setStretch(dockPanel, 1);
376
+ SplitPanel.setStretch(rightHandler.stackedPanel, 0);
377
+
378
+ BoxPanel.setStretch(leftHandler.sideBar, 0);
379
+ BoxPanel.setStretch(hsplitPanel, 1);
380
+ BoxPanel.setStretch(rightHandler.sideBar, 0);
381
+
382
+ SplitPanel.setStretch(vsplitPanel, 1);
383
+
384
+ hsplitPanel.addWidget(leftHandler.stackedPanel);
385
+ hsplitPanel.addWidget(dockPanel);
386
+ hsplitPanel.addWidget(rightHandler.stackedPanel);
387
+
388
+ vsplitPanel.addWidget(hsplitPanel);
389
+ vsplitPanel.addWidget(downPanel);
390
+
391
+ hboxPanel.addWidget(leftHandler.sideBar);
392
+ hboxPanel.addWidget(vsplitPanel);
393
+ hboxPanel.addWidget(rightHandler.sideBar);
394
+
395
+ rootLayout.direction = 'top-to-bottom';
396
+ rootLayout.spacing = 0; // TODO make this configurable?
397
+ // Use relative sizing to set the width of the side panels.
398
+ // This will still respect the min-size of children widget in the stacked
399
+ // panel. The default sizes will be overwritten during layout restoration.
400
+ vsplitPanel.setRelativeSizes([3, 1]);
401
+ hsplitPanel.setRelativeSizes([1, 2.5, 1]);
402
+
403
+ BoxLayout.setStretch(headerPanel, 0);
404
+ BoxLayout.setStretch(menuHandler.panel, 0);
405
+ BoxLayout.setStretch(topHandler.panel, 0);
406
+ BoxLayout.setStretch(hboxPanel, 1);
407
+ BoxLayout.setStretch(bottomPanel, 0);
408
+
409
+ rootLayout.addWidget(skipLinkWrapper);
410
+ rootLayout.addWidget(headerPanel);
411
+ rootLayout.addWidget(topHandler.panel);
412
+ rootLayout.addWidget(hboxPanel);
413
+ rootLayout.addWidget(bottomPanel);
414
+
415
+ // initially hiding header and bottom panel when no elements inside,
416
+ this._headerPanel.hide();
417
+ this._bottomPanel.hide();
418
+ this._downPanel.hide();
419
+
420
+ this.layout = rootLayout;
421
+
422
+ // Connect change listeners.
423
+ this._tracker.currentChanged.connect(this._onCurrentChanged, this);
424
+ this._tracker.activeChanged.connect(this._onActiveChanged, this);
425
+
426
+ // Connect main layout change listener.
427
+ this._dockPanel.layoutModified.connect(this._onLayoutModified, this);
428
+
429
+ // Connect vsplit layout change listener
430
+ this._vsplitPanel.updated.connect(this._onLayoutModified, this);
431
+
432
+ // Connect down panel change listeners
433
+ this._downPanel.currentChanged.connect(this._onLayoutModified, this);
434
+ this._downPanel.tabBar.tabMoved.connect(this._onTabPanelChanged, this);
435
+ this._downPanel.stackedPanel.widgetRemoved.connect(
436
+ this._onTabPanelChanged,
437
+ this
438
+ );
439
+
440
+ // Catch current changed events on the side handlers.
441
+ this._leftHandler.updated.connect(this._onLayoutModified, this);
442
+ this._rightHandler.updated.connect(this._onLayoutModified, this);
443
+
444
+ // Catch update events on the horizontal split panel
445
+ this._hsplitPanel.updated.connect(this._onLayoutModified, this);
446
+
447
+ // Setup single-document-mode title bar
448
+ const titleHandler = (this._titleHandler = new Private.TitleHandler(this));
449
+ this.add(titleHandler, 'top', { rank: 100 });
450
+
451
+ if (this._dockPanel.mode === 'multiple-document') {
452
+ this._topHandler.addWidget(this._menuHandler.panel, 100);
453
+ titleHandler.hide();
454
+ } else {
455
+ rootLayout.insertWidget(3, this._menuHandler.panel);
456
+ }
457
+
458
+ this.translator = nullTranslator;
459
+
460
+ // Wire up signals to update the title panel of the simple interface mode to
461
+ // follow the title of this.currentWidget
462
+ this.currentChanged.connect((sender, args) => {
463
+ let newValue = args.newValue;
464
+ let oldValue = args.oldValue;
465
+
466
+ // Stop watching the title of the previously current widget
467
+ if (oldValue) {
468
+ oldValue.title.changed.disconnect(this._updateTitlePanelTitle, this);
469
+ }
470
+
471
+ // Start watching the title of the new current widget
472
+ if (newValue) {
473
+ newValue.title.changed.connect(this._updateTitlePanelTitle, this);
474
+ this._updateTitlePanelTitle();
475
+ }
476
+
477
+ if (newValue && newValue instanceof DocumentWidget) {
478
+ newValue.context.pathChanged.connect(this._updateCurrentPath, this);
479
+ }
480
+ this._updateCurrentPath();
481
+ });
482
+ }
483
+
484
+ /**
485
+ * A signal emitted when main area's active focus changes.
486
+ */
487
+ get activeChanged(): ISignal<this, ILabShell.IChangedArgs> {
488
+ return this._activeChanged;
489
+ }
490
+
491
+ /**
492
+ * The active widget in the shell's main area.
493
+ */
494
+ get activeWidget(): Widget | null {
495
+ return this._tracker.activeWidget;
496
+ }
497
+
498
+ /**
499
+ * Whether the add buttons for each main area tab bar are enabled.
500
+ */
501
+ get addButtonEnabled(): boolean {
502
+ return this._dockPanel.addButtonEnabled;
503
+ }
504
+ set addButtonEnabled(value: boolean) {
505
+ this._dockPanel.addButtonEnabled = value;
506
+ }
507
+
508
+ /**
509
+ * A signal emitted when the add button on a main area tab bar is clicked.
510
+ */
511
+ get addRequested(): ISignal<DockPanel, TabBar<Widget>> {
512
+ return this._dockPanel.addRequested;
513
+ }
514
+
515
+ /**
516
+ * A signal emitted when main area's current focus changes.
517
+ */
518
+ get currentChanged(): ISignal<this, ILabShell.IChangedArgs> {
519
+ return this._currentChanged;
520
+ }
521
+
522
+ /**
523
+ * A signal emitted when the path of the current document changes.
524
+ *
525
+ * This also fires when the current document itself changes.
526
+ */
527
+ get currentPathChanged(): ISignal<this, ILabShell.ICurrentPathChangedArgs> {
528
+ return this._currentPathChanged;
529
+ }
530
+
531
+ /**
532
+ * The current widget in the shell's main area.
533
+ */
534
+ get currentWidget(): Widget | null {
535
+ return this._tracker.currentWidget;
536
+ }
537
+
538
+ /**
539
+ * A signal emitted when the main area's layout is modified.
540
+ */
541
+ get layoutModified(): ISignal<this, void> {
542
+ return this._layoutModified;
543
+ }
544
+
545
+ /**
546
+ * Whether the left area is collapsed.
547
+ */
548
+ get leftCollapsed(): boolean {
549
+ return !this._leftHandler.sideBar.currentTitle;
550
+ }
551
+
552
+ /**
553
+ * Whether the left area is collapsed.
554
+ */
555
+ get rightCollapsed(): boolean {
556
+ return !this._rightHandler.sideBar.currentTitle;
557
+ }
558
+
559
+ /**
560
+ * Whether JupyterLab is in presentation mode with the
561
+ * `jp-mod-presentationMode` CSS class.
562
+ */
563
+ get presentationMode(): boolean {
564
+ return this.hasClass('jp-mod-presentationMode');
565
+ }
566
+ set presentationMode(value: boolean) {
567
+ this.toggleClass('jp-mod-presentationMode', value);
568
+ }
569
+
570
+ /**
571
+ * The main dock area's user interface mode.
572
+ */
573
+ get mode(): DockPanel.Mode {
574
+ return this._dockPanel.mode;
575
+ }
576
+ set mode(mode: DockPanel.Mode) {
577
+ const dock = this._dockPanel;
578
+ if (mode === dock.mode) {
579
+ return;
580
+ }
581
+
582
+ const applicationCurrentWidget = this.currentWidget;
583
+
584
+ if (mode === 'single-document') {
585
+ // Cache the current multi-document layout before changing the mode.
586
+ this._cachedLayout = dock.saveLayout();
587
+ dock.mode = mode;
588
+
589
+ // In case the active widget in the dock panel is *not* the active widget
590
+ // of the application, defer to the application.
591
+ if (this.currentWidget) {
592
+ dock.activateWidget(this.currentWidget);
593
+ }
594
+
595
+ // Adjust menu and title
596
+ (this.layout as BoxLayout).insertWidget(3, this._menuHandler.panel);
597
+ this._titleHandler.show();
598
+ this._updateTitlePanelTitle();
599
+ if (this._topHandlerHiddenByUser) {
600
+ this._topHandler.panel.hide();
601
+ }
602
+ } else {
603
+ // Cache a reference to every widget currently in the dock panel before
604
+ // changing its mode.
605
+ const widgets = Array.from(dock.widgets());
606
+ dock.mode = mode;
607
+
608
+ // Restore cached layout if possible.
609
+ if (this._cachedLayout) {
610
+ // Remove any disposed widgets in the cached layout and restore.
611
+ Private.normalizeAreaConfig(dock, this._cachedLayout.main);
612
+ dock.restoreLayout(this._cachedLayout);
613
+ this._cachedLayout = null;
614
+ }
615
+
616
+ // If layout restoration has been deferred, restore layout now.
617
+ if (this._layoutRestorer.isDeferred) {
618
+ this._layoutRestorer
619
+ .restoreDeferred()
620
+ .then(mainArea => {
621
+ if (mainArea) {
622
+ const { currentWidget, dock } = mainArea;
623
+ if (dock) {
624
+ this._dockPanel.restoreLayout(dock);
625
+ }
626
+ if (currentWidget) {
627
+ this.activateById(currentWidget.id);
628
+ }
629
+ }
630
+ })
631
+ .catch(reason => {
632
+ console.error('Failed to restore the deferred layout.');
633
+ console.error(reason);
634
+ });
635
+ }
636
+
637
+ // Add any widgets created during single document mode, which have
638
+ // subsequently been removed from the dock panel after the multiple document
639
+ // layout has been restored. If the widget has add options cached for
640
+ // the widget (i.e., if it has been placed with respect to another widget),
641
+ // then take that into account.
642
+ widgets.forEach(widget => {
643
+ if (!widget.parent) {
644
+ this._addToMainArea(widget, {
645
+ ...this._mainOptionsCache.get(widget),
646
+ activate: false
647
+ });
648
+ }
649
+ });
650
+ this._mainOptionsCache.clear();
651
+
652
+ // In case the active widget in the dock panel is *not* the active widget
653
+ // of the application, defer to the application.
654
+ if (applicationCurrentWidget) {
655
+ dock.activateWidget(applicationCurrentWidget);
656
+ }
657
+
658
+ // Adjust menu and title
659
+ this.add(this._menuHandler.panel, 'top', { rank: 100 });
660
+ this._titleHandler.hide();
661
+ }
662
+
663
+ // Set the mode data attribute on the applications shell node.
664
+ this.node.dataset.shellMode = mode;
665
+
666
+ this._downPanel.fit();
667
+ // Emit the mode changed signal
668
+ this._modeChanged.emit(mode);
669
+ }
670
+
671
+ /**
672
+ * A signal emitted when the shell/dock panel change modes (single/multiple document).
673
+ */
674
+ get modeChanged(): ISignal<this, DockPanel.Mode> {
675
+ return this._modeChanged;
676
+ }
677
+
678
+ /**
679
+ * Promise that resolves when state is first restored, returning layout
680
+ * description.
681
+ */
682
+ get restored(): Promise<ILabShell.ILayout> {
683
+ return this._restored.promise;
684
+ }
685
+
686
+ get translator(): ITranslator {
687
+ return this._translator ?? nullTranslator;
688
+ }
689
+ set translator(value: ITranslator) {
690
+ if (value !== this._translator) {
691
+ this._translator = value;
692
+
693
+ // Set translator for tab bars
694
+ TabBarSvg.translator = value;
695
+
696
+ const trans = value.load('jupyterlab');
697
+ this._menuHandler.panel.node.setAttribute('aria-label', trans.__('main'));
698
+ this._leftHandler.sideBar.node.setAttribute(
699
+ 'aria-label',
700
+ trans.__('main sidebar')
701
+ );
702
+ this._leftHandler.sideBar.contentNode.setAttribute(
703
+ 'aria-label',
704
+ trans.__('main sidebar')
705
+ );
706
+ this._rightHandler.sideBar.node.setAttribute(
707
+ 'aria-label',
708
+ trans.__('alternate sidebar')
709
+ );
710
+ this._rightHandler.sideBar.contentNode.setAttribute(
711
+ 'aria-label',
712
+ trans.__('alternate sidebar')
713
+ );
714
+ }
715
+ }
716
+
717
+ /**
718
+ * User customized shell layout.
719
+ */
720
+ get userLayout(): {
721
+ 'single-document': ILabShell.IUserLayout;
722
+ 'multiple-document': ILabShell.IUserLayout;
723
+ } {
724
+ return JSONExt.deepCopy(this._userLayout as any);
725
+ }
726
+
727
+ /**
728
+ * Activate a widget in its area.
729
+ */
730
+ activateById(id: string): void {
731
+ if (this._leftHandler.has(id)) {
732
+ this._leftHandler.activate(id);
733
+ return;
734
+ }
735
+
736
+ if (this._rightHandler.has(id)) {
737
+ this._rightHandler.activate(id);
738
+ return;
739
+ }
740
+
741
+ const tabIndex = this._downPanel.tabBar.titles.findIndex(
742
+ title => title.owner.id === id
743
+ );
744
+ if (tabIndex >= 0) {
745
+ this._downPanel.currentIndex = tabIndex;
746
+ return;
747
+ }
748
+
749
+ const dock = this._dockPanel;
750
+ const widget = find(dock.widgets(), value => value.id === id);
751
+
752
+ if (widget) {
753
+ dock.activateWidget(widget);
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Activate the next Tab in the active TabBar.
759
+ */
760
+ activateNextTab(): void {
761
+ const current = this._currentTabBar();
762
+ if (!current) {
763
+ return;
764
+ }
765
+
766
+ const ci = current.currentIndex;
767
+ if (ci === -1) {
768
+ return;
769
+ }
770
+
771
+ if (ci < current.titles.length - 1) {
772
+ current.currentIndex += 1;
773
+ if (current.currentTitle) {
774
+ current.currentTitle.owner.activate();
775
+ }
776
+ return;
777
+ }
778
+
779
+ if (ci === current.titles.length - 1) {
780
+ const nextBar = this._adjacentBar('next');
781
+ if (nextBar) {
782
+ nextBar.currentIndex = 0;
783
+ if (nextBar.currentTitle) {
784
+ nextBar.currentTitle.owner.activate();
785
+ }
786
+ }
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Activate the previous Tab in the active TabBar.
792
+ */
793
+ activatePreviousTab(): void {
794
+ const current = this._currentTabBar();
795
+ if (!current) {
796
+ return;
797
+ }
798
+
799
+ const ci = current.currentIndex;
800
+ if (ci === -1) {
801
+ return;
802
+ }
803
+
804
+ if (ci > 0) {
805
+ current.currentIndex -= 1;
806
+ if (current.currentTitle) {
807
+ current.currentTitle.owner.activate();
808
+ }
809
+ return;
810
+ }
811
+
812
+ if (ci === 0) {
813
+ const prevBar = this._adjacentBar('previous');
814
+ if (prevBar) {
815
+ const len = prevBar.titles.length;
816
+ prevBar.currentIndex = len - 1;
817
+ if (prevBar.currentTitle) {
818
+ prevBar.currentTitle.owner.activate();
819
+ }
820
+ }
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Activate the next TabBar.
826
+ */
827
+ activateNextTabBar(): void {
828
+ const nextBar = this._adjacentBar('next');
829
+ if (nextBar) {
830
+ if (nextBar.currentTitle) {
831
+ nextBar.currentTitle.owner.activate();
832
+ }
833
+ }
834
+ }
835
+
836
+ /**
837
+ * Activate the next TabBar.
838
+ */
839
+ activatePreviousTabBar(): void {
840
+ const nextBar = this._adjacentBar('previous');
841
+ if (nextBar) {
842
+ if (nextBar.currentTitle) {
843
+ nextBar.currentTitle.owner.activate();
844
+ }
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Add a widget to the JupyterLab shell
850
+ *
851
+ * @param widget Widget
852
+ * @param area Area
853
+ * @param options Options
854
+ */
855
+ add(
856
+ widget: Widget,
857
+ area: ILabShell.Area = 'main',
858
+ options?: DocumentRegistry.IOpenOptions
859
+ ): void {
860
+ if (!this._userLayout) {
861
+ this._delayedWidget.push({ widget, area, options });
862
+ return;
863
+ }
864
+
865
+ let userPosition: ILabShell.IWidgetPosition | undefined;
866
+ if (options?.type && this._userLayout[this.mode][options.type]) {
867
+ userPosition = this._userLayout[this.mode][options.type];
868
+ this._idTypeMap.set(widget.id, options.type);
869
+ } else {
870
+ userPosition = this._userLayout[this.mode][widget.id];
871
+ }
872
+ if (options?.type) {
873
+ this._idTypeMap.set(widget.id, options.type);
874
+ widget.disposed.connect(() => {
875
+ this._idTypeMap.delete(widget.id);
876
+ });
877
+ }
878
+
879
+ area = userPosition?.area ?? area;
880
+ options =
881
+ options || userPosition?.options
882
+ ? {
883
+ ...options,
884
+ ...userPosition?.options
885
+ }
886
+ : undefined;
887
+
888
+ switch (area || 'main') {
889
+ case 'bottom':
890
+ return this._addToBottomArea(widget, options);
891
+ case 'down':
892
+ return this._addToDownArea(widget, options);
893
+ case 'header':
894
+ return this._addToHeaderArea(widget, options);
895
+ case 'left':
896
+ return this._addToLeftArea(widget, options);
897
+ case 'main':
898
+ return this._addToMainArea(widget, options);
899
+ case 'menu':
900
+ return this._addToMenuArea(widget, options);
901
+ case 'right':
902
+ return this._addToRightArea(widget, options);
903
+ case 'top':
904
+ return this._addToTopArea(widget, options);
905
+ default:
906
+ throw new Error(`Invalid area: ${area}`);
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Move a widget type to a new area.
912
+ *
913
+ * The type is determined from the `widget.id` and fallback to `widget.id`.
914
+ *
915
+ * #### Notes
916
+ * If `mode` is undefined, both mode are updated.
917
+ * The new layout is now persisted.
918
+ *
919
+ * @param widget Widget to move
920
+ * @param area New area
921
+ * @param mode Mode to change
922
+ * @returns The new user layout
923
+ */
924
+ move(
925
+ widget: Widget,
926
+ area: ILabShell.Area,
927
+ mode?: DockPanel.Mode
928
+ ): {
929
+ 'single-document': ILabShell.IUserLayout;
930
+ 'multiple-document': ILabShell.IUserLayout;
931
+ } {
932
+ const type = this._idTypeMap.get(widget.id) ?? widget.id;
933
+ for (const m of ['single-document', 'multiple-document'].filter(
934
+ c => !mode || c === mode
935
+ )) {
936
+ this._userLayout[m as DockPanel.Mode][type] = {
937
+ ...this._userLayout[m as DockPanel.Mode][type],
938
+ area
939
+ };
940
+ }
941
+
942
+ this.add(widget, area);
943
+
944
+ return this._userLayout;
945
+ }
946
+
947
+ /**
948
+ * Collapse the left area.
949
+ */
950
+ collapseLeft(): void {
951
+ this._leftHandler.collapse();
952
+ this._onLayoutModified();
953
+ }
954
+
955
+ /**
956
+ * Collapse the right area.
957
+ */
958
+ collapseRight(): void {
959
+ this._rightHandler.collapse();
960
+ this._onLayoutModified();
961
+ }
962
+
963
+ /**
964
+ * Dispose the shell.
965
+ */
966
+ dispose(): void {
967
+ if (this.isDisposed) {
968
+ return;
969
+ }
970
+ this._layoutDebouncer.dispose();
971
+ super.dispose();
972
+ }
973
+
974
+ /**
975
+ * Expand the left area.
976
+ *
977
+ * #### Notes
978
+ * This will open the most recently used tab,
979
+ * or the first tab if there is no most recently used.
980
+ */
981
+ expandLeft(): void {
982
+ this._leftHandler.expand();
983
+ this._onLayoutModified();
984
+ }
985
+
986
+ /**
987
+ * Expand the right area.
988
+ *
989
+ * #### Notes
990
+ * This will open the most recently used tab,
991
+ * or the first tab if there is no most recently used.
992
+ */
993
+ expandRight(): void {
994
+ this._rightHandler.expand();
995
+ this._onLayoutModified();
996
+ }
997
+
998
+ /**
999
+ * Close all widgets in the main and down area.
1000
+ */
1001
+ closeAll(): void {
1002
+ // Make a copy of all the widget in the dock panel (using `Array.from()`)
1003
+ // before removing them because removing them while iterating through them
1004
+ // modifies the underlying data of the iterator.
1005
+ Array.from(this._dockPanel.widgets()).forEach(widget => widget.close());
1006
+
1007
+ this._downPanel.stackedPanel.widgets.forEach(widget => widget.close());
1008
+ }
1009
+
1010
+ /**
1011
+ * Whether an side tab bar is visible or not.
1012
+ *
1013
+ * @param side Sidebar of interest
1014
+ * @returns Side tab bar visibility
1015
+ */
1016
+ isSideTabBarVisible(side: 'left' | 'right'): boolean {
1017
+ switch (side) {
1018
+ case 'left':
1019
+ return this._leftHandler.isVisible;
1020
+ case 'right':
1021
+ return this._rightHandler.isVisible;
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Whether the top bar in simple mode is visible or not.
1027
+ *
1028
+ * @returns Top bar visibility
1029
+ */
1030
+ isTopInSimpleModeVisible(): boolean {
1031
+ return !this._topHandlerHiddenByUser;
1032
+ }
1033
+
1034
+ /**
1035
+ * True if the given area is empty.
1036
+ */
1037
+ isEmpty(area: ILabShell.Area): boolean {
1038
+ switch (area) {
1039
+ case 'bottom':
1040
+ return this._bottomPanel.widgets.length === 0;
1041
+ case 'down':
1042
+ return this._downPanel.stackedPanel.widgets.length === 0;
1043
+ case 'header':
1044
+ return this._headerPanel.widgets.length === 0;
1045
+ case 'left':
1046
+ return this._leftHandler.stackedPanel.widgets.length === 0;
1047
+ case 'main':
1048
+ return this._dockPanel.isEmpty;
1049
+ case 'menu':
1050
+ return this._menuHandler.panel.widgets.length === 0;
1051
+ case 'right':
1052
+ return this._rightHandler.stackedPanel.widgets.length === 0;
1053
+ case 'top':
1054
+ return this._topHandler.panel.widgets.length === 0;
1055
+ default:
1056
+ return true;
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Restore the layout state and configuration for the application shell.
1062
+ *
1063
+ * #### Notes
1064
+ * This should only be called once.
1065
+ */
1066
+ async restoreLayout(
1067
+ mode: DockPanel.Mode,
1068
+ layoutRestorer: LayoutRestorer,
1069
+ configuration: {
1070
+ [m: string]: ILabShell.IUserLayout;
1071
+ } = {}
1072
+ ): Promise<void> {
1073
+ // Set the configuration and add widgets added before the shell was ready.
1074
+ this._userLayout = {
1075
+ 'single-document': configuration['single-document'] ?? {},
1076
+ 'multiple-document': configuration['multiple-document'] ?? {}
1077
+ };
1078
+ this._delayedWidget.forEach(({ widget, area, options }) => {
1079
+ this.add(widget, area, options);
1080
+ });
1081
+ this._delayedWidget.length = 0;
1082
+ this._layoutRestorer = layoutRestorer;
1083
+
1084
+ // Get the layout from the restorer
1085
+ const layout = await layoutRestorer.fetch();
1086
+
1087
+ // Reset the layout
1088
+ const { mainArea, downArea, leftArea, rightArea, topArea, relativeSizes } =
1089
+ layout;
1090
+
1091
+ // Rehydrate the main area.
1092
+ if (mainArea) {
1093
+ const { currentWidget, dock } = mainArea;
1094
+
1095
+ if (dock && mode === 'multiple-document') {
1096
+ this._dockPanel.restoreLayout(dock);
1097
+ }
1098
+ if (mode) {
1099
+ this.mode = mode;
1100
+ }
1101
+ if (currentWidget) {
1102
+ this.activateById(currentWidget.id);
1103
+ }
1104
+ } else {
1105
+ // This is needed when loading in an empty workspace in single doc mode
1106
+ if (mode) {
1107
+ this.mode = mode;
1108
+ }
1109
+ }
1110
+
1111
+ if (topArea?.simpleVisibility !== undefined) {
1112
+ this._topHandlerHiddenByUser = !topArea.simpleVisibility;
1113
+ if (this.mode === 'single-document') {
1114
+ this._topHandler.panel.setHidden(this._topHandlerHiddenByUser);
1115
+ }
1116
+ }
1117
+
1118
+ // Rehydrate the down area
1119
+ if (downArea) {
1120
+ const { currentWidget, widgets, size } = downArea;
1121
+
1122
+ const widgetIds = widgets?.map(widget => widget.id) ?? [];
1123
+ // Remove absent widgets
1124
+ this._downPanel.tabBar.titles
1125
+ .filter(title => !widgetIds.includes(title.owner.id))
1126
+ .map(title => title.owner.close());
1127
+ // Add new widgets
1128
+ const titleIds = this._downPanel.tabBar.titles.map(
1129
+ title => title.owner.id
1130
+ );
1131
+ widgets
1132
+ ?.filter(widget => !titleIds.includes(widget.id))
1133
+ .map(widget => this._downPanel.addWidget(widget));
1134
+ // Reorder tabs
1135
+ while (
1136
+ !ArrayExt.shallowEqual(
1137
+ widgetIds,
1138
+ this._downPanel.tabBar.titles.map(title => title.owner.id)
1139
+ )
1140
+ ) {
1141
+ this._downPanel.tabBar.titles.forEach((title, index) => {
1142
+ const position = widgetIds.findIndex(id => title.owner.id == id);
1143
+ if (position >= 0 && position != index) {
1144
+ this._downPanel.tabBar.insertTab(position, title);
1145
+ }
1146
+ });
1147
+ }
1148
+
1149
+ if (currentWidget) {
1150
+ const index = this._downPanel.stackedPanel.widgets.findIndex(
1151
+ widget => widget.id === currentWidget.id
1152
+ );
1153
+ if (index) {
1154
+ this._downPanel.currentIndex = index;
1155
+ this._downPanel.currentWidget?.activate();
1156
+ }
1157
+ }
1158
+
1159
+ if (size && size > 0.0) {
1160
+ this._vsplitPanel.setRelativeSizes([1.0 - size, size]);
1161
+ } else {
1162
+ // Close all tabs and hide the panel
1163
+ this._downPanel.stackedPanel.widgets.forEach(widget => widget.close());
1164
+ this._downPanel.hide();
1165
+ }
1166
+ }
1167
+
1168
+ // Rehydrate the left area.
1169
+ if (leftArea) {
1170
+ this._leftHandler.rehydrate(leftArea);
1171
+ } else {
1172
+ if (mode === 'single-document') {
1173
+ this.collapseLeft();
1174
+ }
1175
+ }
1176
+
1177
+ // Rehydrate the right area.
1178
+ if (rightArea) {
1179
+ this._rightHandler.rehydrate(rightArea);
1180
+ } else {
1181
+ if (mode === 'single-document') {
1182
+ this.collapseRight();
1183
+ }
1184
+ }
1185
+
1186
+ // Restore the relative sizes.
1187
+ if (relativeSizes) {
1188
+ this._hsplitPanel.setRelativeSizes(relativeSizes);
1189
+ }
1190
+
1191
+ if (!this._isRestored) {
1192
+ // Make sure all messages in the queue are finished before notifying
1193
+ // any extensions that are waiting for the promise that guarantees the
1194
+ // application state has been restored.
1195
+ MessageLoop.flush();
1196
+ this._restored.resolve(layout);
1197
+ }
1198
+ }
1199
+
1200
+ /**
1201
+ * Save the dehydrated state of the application shell.
1202
+ */
1203
+ saveLayout(): ILabShell.ILayout {
1204
+ // If the application is in single document mode, use the cached layout if
1205
+ // available. Otherwise, default to querying the dock panel for layout.
1206
+ const layout = {
1207
+ mainArea: {
1208
+ currentWidget: this._tracker.currentWidget,
1209
+ dock:
1210
+ this.mode === 'single-document'
1211
+ ? this._cachedLayout || this._dockPanel.saveLayout()
1212
+ : this._dockPanel.saveLayout()
1213
+ },
1214
+ downArea: {
1215
+ currentWidget: this._downPanel.currentWidget,
1216
+ widgets: Array.from(this._downPanel.stackedPanel.widgets),
1217
+ size: this._vsplitPanel.relativeSizes()[1]
1218
+ },
1219
+ leftArea: this._leftHandler.dehydrate(),
1220
+ rightArea: this._rightHandler.dehydrate(),
1221
+ topArea: { simpleVisibility: !this._topHandlerHiddenByUser },
1222
+ relativeSizes: this._hsplitPanel.relativeSizes()
1223
+ };
1224
+ return layout;
1225
+ }
1226
+
1227
+ /**
1228
+ * Toggle top header visibility in simple mode
1229
+ *
1230
+ * Note: Does nothing in multi-document mode
1231
+ */
1232
+ toggleTopInSimpleModeVisibility(): void {
1233
+ if (this.mode === 'single-document') {
1234
+ if (this._topHandler.panel.isVisible) {
1235
+ this._topHandlerHiddenByUser = true;
1236
+ this._topHandler.panel.hide();
1237
+ } else {
1238
+ this._topHandlerHiddenByUser = false;
1239
+ this._topHandler.panel.show();
1240
+
1241
+ this._updateTitlePanelTitle();
1242
+ }
1243
+ this._onLayoutModified();
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * Toggle side tab bar visibility
1249
+ *
1250
+ * @param side Sidebar of interest
1251
+ */
1252
+ toggleSideTabBarVisibility(side: 'right' | 'left'): void {
1253
+ if (side === 'right') {
1254
+ if (this._rightHandler.isVisible) {
1255
+ this._rightHandler.hide();
1256
+ } else {
1257
+ this._rightHandler.show();
1258
+ }
1259
+ } else {
1260
+ if (this._leftHandler.isVisible) {
1261
+ this._leftHandler.hide();
1262
+ } else {
1263
+ this._leftHandler.show();
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ /**
1269
+ * Update the shell configuration.
1270
+ *
1271
+ * @param config Shell configuration
1272
+ */
1273
+ updateConfig(config: Partial<ILabShell.IConfig>): void {
1274
+ if (config.hiddenMode) {
1275
+ switch (config.hiddenMode) {
1276
+ case 'display':
1277
+ this._dockPanel.hiddenMode = Widget.HiddenMode.Display;
1278
+ break;
1279
+ case 'scale':
1280
+ this._dockPanel.hiddenMode = Widget.HiddenMode.Scale;
1281
+ break;
1282
+ case 'contentVisibility':
1283
+ this._dockPanel.hiddenMode = Widget.HiddenMode.ContentVisibility;
1284
+ break;
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ /**
1290
+ * Returns the widgets for an application area.
1291
+ */
1292
+ widgets(area?: ILabShell.Area): IterableIterator<Widget> {
1293
+ switch (area ?? 'main') {
1294
+ case 'main':
1295
+ return this._dockPanel.widgets();
1296
+ case 'left':
1297
+ return map(this._leftHandler.sideBar.titles, t => t.owner);
1298
+ case 'right':
1299
+ return map(this._rightHandler.sideBar.titles, t => t.owner);
1300
+ case 'header':
1301
+ return this._headerPanel.children();
1302
+ case 'top':
1303
+ return this._topHandler.panel.children();
1304
+ case 'menu':
1305
+ return this._menuHandler.panel.children();
1306
+ case 'bottom':
1307
+ return this._bottomPanel.children();
1308
+ default:
1309
+ throw new Error(`Invalid area: ${area}`);
1310
+ }
1311
+ }
1312
+
1313
+ /**
1314
+ * Handle `after-attach` messages for the application shell.
1315
+ */
1316
+ protected onAfterAttach(msg: Message): void {
1317
+ this.node.dataset.shellMode = this.mode;
1318
+ }
1319
+
1320
+ /**
1321
+ * Update the title panel title based on the title of the current widget.
1322
+ */
1323
+ private _updateTitlePanelTitle() {
1324
+ let current = this.currentWidget;
1325
+ const inputElement = this._titleHandler.inputElement;
1326
+ inputElement.value = current ? current.title.label : '';
1327
+ inputElement.title = current ? current.title.caption : '';
1328
+ }
1329
+
1330
+ /**
1331
+ * The path of the current widget changed, fire the _currentPathChanged signal.
1332
+ */
1333
+ private _updateCurrentPath() {
1334
+ let current = this.currentWidget;
1335
+ let newValue = '';
1336
+ if (current && current instanceof DocumentWidget) {
1337
+ newValue = current.context.path;
1338
+ }
1339
+ this._currentPathChanged.emit({
1340
+ newValue: newValue,
1341
+ oldValue: this._currentPath
1342
+ });
1343
+ this._currentPath = newValue;
1344
+ }
1345
+
1346
+ /**
1347
+ * Add a widget to the left content area.
1348
+ *
1349
+ * #### Notes
1350
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1351
+ */
1352
+ private _addToLeftArea(
1353
+ widget: Widget,
1354
+ options?: DocumentRegistry.IOpenOptions
1355
+ ): void {
1356
+ if (!widget.id) {
1357
+ console.error('Widgets added to app shell must have unique id property.');
1358
+ return;
1359
+ }
1360
+ options = options || this._sideOptionsCache.get(widget) || {};
1361
+ this._sideOptionsCache.set(widget, options);
1362
+ const rank = 'rank' in options ? options.rank : DEFAULT_RANK;
1363
+ this._leftHandler.addWidget(widget, rank!);
1364
+ this._onLayoutModified();
1365
+ }
1366
+
1367
+ /**
1368
+ * Add a widget to the main content area.
1369
+ *
1370
+ * #### Notes
1371
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1372
+ * All widgets added to the main area should be disposed after removal
1373
+ * (disposal before removal will remove the widget automatically).
1374
+ *
1375
+ * In the options, `ref` defaults to `null`, `mode` defaults to `'tab-after'`,
1376
+ * and `activate` defaults to `true`.
1377
+ */
1378
+ private _addToMainArea(
1379
+ widget: Widget,
1380
+ options?: DocumentRegistry.IOpenOptions
1381
+ ): void {
1382
+ if (!widget.id) {
1383
+ console.error('Widgets added to app shell must have unique id property.');
1384
+ return;
1385
+ }
1386
+
1387
+ options = options || {};
1388
+
1389
+ const dock = this._dockPanel;
1390
+ const mode = options.mode || 'tab-after';
1391
+ let ref: Widget | null = this.currentWidget;
1392
+
1393
+ if (options.ref) {
1394
+ ref = find(dock.widgets(), value => value.id === options!.ref!) || null;
1395
+ }
1396
+
1397
+ const { title } = widget;
1398
+ // Add widget ID to tab so that we can get a handle on the tab's widget
1399
+ // (for context menu support)
1400
+ title.dataset = { ...title.dataset, id: widget.id };
1401
+
1402
+ if (title.icon instanceof LabIcon) {
1403
+ // bind an appropriate style to the icon
1404
+ title.icon = title.icon.bindprops({
1405
+ stylesheet: 'mainAreaTab'
1406
+ });
1407
+ } else if (typeof title.icon === 'string' || !title.icon) {
1408
+ // add some classes to help with displaying css background imgs
1409
+ title.iconClass = classes(title.iconClass, 'jp-Icon');
1410
+ }
1411
+
1412
+ dock.addWidget(widget, { mode, ref });
1413
+
1414
+ // The dock panel doesn't account for placement information while
1415
+ // in single document mode, so upon rehydrating any widgets that were
1416
+ // added will not be in the correct place. Cache the placement information
1417
+ // here so that we can later rehydrate correctly.
1418
+ if (dock.mode === 'single-document') {
1419
+ this._mainOptionsCache.set(widget, options);
1420
+ }
1421
+
1422
+ if (options.activate !== false) {
1423
+ dock.activateWidget(widget);
1424
+ }
1425
+ }
1426
+
1427
+ /**
1428
+ * Add a widget to the right content area.
1429
+ *
1430
+ * #### Notes
1431
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1432
+ */
1433
+ private _addToRightArea(
1434
+ widget: Widget,
1435
+ options?: DocumentRegistry.IOpenOptions
1436
+ ): void {
1437
+ if (!widget.id) {
1438
+ console.error('Widgets added to app shell must have unique id property.');
1439
+ return;
1440
+ }
1441
+ options = options || this._sideOptionsCache.get(widget) || {};
1442
+
1443
+ const rank = 'rank' in options ? options.rank : DEFAULT_RANK;
1444
+
1445
+ this._sideOptionsCache.set(widget, options);
1446
+ this._rightHandler.addWidget(widget, rank!);
1447
+ this._onLayoutModified();
1448
+ }
1449
+
1450
+ /**
1451
+ * Add a widget to the top content area.
1452
+ *
1453
+ * #### Notes
1454
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1455
+ */
1456
+ private _addToTopArea(
1457
+ widget: Widget,
1458
+ options?: DocumentRegistry.IOpenOptions
1459
+ ): void {
1460
+ if (!widget.id) {
1461
+ console.error('Widgets added to app shell must have unique id property.');
1462
+ return;
1463
+ }
1464
+ options = options || {};
1465
+ const rank = options.rank ?? DEFAULT_RANK;
1466
+ this._topHandler.addWidget(widget, rank);
1467
+ this._onLayoutModified();
1468
+ if (this._topHandler.panel.isHidden) {
1469
+ this._topHandler.panel.show();
1470
+ }
1471
+ }
1472
+
1473
+ /**
1474
+ * Add a widget to the title content area.
1475
+ *
1476
+ * #### Notes
1477
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1478
+ */
1479
+ private _addToMenuArea(
1480
+ widget: Widget,
1481
+ options?: DocumentRegistry.IOpenOptions
1482
+ ): void {
1483
+ if (!widget.id) {
1484
+ console.error('Widgets added to app shell must have unique id property.');
1485
+ return;
1486
+ }
1487
+ options = options || {};
1488
+ const rank = options.rank ?? DEFAULT_RANK;
1489
+ this._menuHandler.addWidget(widget, rank);
1490
+ this._onLayoutModified();
1491
+ if (this._menuHandler.panel.isHidden) {
1492
+ this._menuHandler.panel.show();
1493
+ }
1494
+ }
1495
+
1496
+ /**
1497
+ * Add a widget to the header content area.
1498
+ *
1499
+ * #### Notes
1500
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1501
+ */
1502
+ private _addToHeaderArea(
1503
+ widget: Widget,
1504
+ options?: DocumentRegistry.IOpenOptions
1505
+ ): void {
1506
+ if (!widget.id) {
1507
+ console.error('Widgets added to app shell must have unique id property.');
1508
+ return;
1509
+ }
1510
+ // Temporary: widgets are added to the panel in order of insertion.
1511
+ this._headerPanel.addWidget(widget);
1512
+ this._onLayoutModified();
1513
+
1514
+ if (this._headerPanel.isHidden) {
1515
+ this._headerPanel.show();
1516
+ }
1517
+ }
1518
+ /**
1519
+ * Add a widget to the bottom content area.
1520
+ *
1521
+ * #### Notes
1522
+ * Widgets must have a unique `id` property, which will be used as the DOM id.
1523
+ */
1524
+ private _addToBottomArea(
1525
+ widget: Widget,
1526
+ options?: DocumentRegistry.IOpenOptions
1527
+ ): void {
1528
+ if (!widget.id) {
1529
+ console.error('Widgets added to app shell must have unique id property.');
1530
+ return;
1531
+ }
1532
+ // Temporary: widgets are added to the panel in order of insertion.
1533
+ this._bottomPanel.addWidget(widget);
1534
+ this._onLayoutModified();
1535
+
1536
+ if (this._bottomPanel.isHidden) {
1537
+ this._bottomPanel.show();
1538
+ }
1539
+ }
1540
+
1541
+ private _addToDownArea(
1542
+ widget: Widget,
1543
+ options?: DocumentRegistry.IOpenOptions
1544
+ ): void {
1545
+ if (!widget.id) {
1546
+ console.error('Widgets added to app shell must have unique id property.');
1547
+ return;
1548
+ }
1549
+
1550
+ options = options || {};
1551
+
1552
+ const { title } = widget;
1553
+ // Add widget ID to tab so that we can get a handle on the tab's widget
1554
+ // (for context menu support)
1555
+ title.dataset = { ...title.dataset, id: widget.id };
1556
+
1557
+ if (title.icon instanceof LabIcon) {
1558
+ // bind an appropriate style to the icon
1559
+ title.icon = title.icon.bindprops({
1560
+ stylesheet: 'mainAreaTab'
1561
+ });
1562
+ } else if (typeof title.icon === 'string' || !title.icon) {
1563
+ // add some classes to help with displaying css background imgs
1564
+ title.iconClass = classes(title.iconClass, 'jp-Icon');
1565
+ }
1566
+
1567
+ this._downPanel.addWidget(widget);
1568
+ this._onLayoutModified();
1569
+
1570
+ if (this._downPanel.isHidden) {
1571
+ this._downPanel.show();
1572
+ }
1573
+ }
1574
+
1575
+ /*
1576
+ * Return the tab bar adjacent to the current TabBar or `null`.
1577
+ */
1578
+ private _adjacentBar(direction: 'next' | 'previous'): TabBar<Widget> | null {
1579
+ const current = this._currentTabBar();
1580
+ if (!current) {
1581
+ return null;
1582
+ }
1583
+
1584
+ const bars = Array.from(this._dockPanel.tabBars());
1585
+ const len = bars.length;
1586
+ const index = bars.indexOf(current);
1587
+
1588
+ if (direction === 'previous') {
1589
+ return index > 0 ? bars[index - 1] : index === 0 ? bars[len - 1] : null;
1590
+ }
1591
+
1592
+ // Otherwise, direction is 'next'.
1593
+ return index < len - 1
1594
+ ? bars[index + 1]
1595
+ : index === len - 1
1596
+ ? bars[0]
1597
+ : null;
1598
+ }
1599
+
1600
+ /*
1601
+ * Return the TabBar that has the currently active Widget or null.
1602
+ */
1603
+ private _currentTabBar(): TabBar<Widget> | null {
1604
+ const current = this._tracker.currentWidget;
1605
+ if (!current) {
1606
+ return null;
1607
+ }
1608
+
1609
+ const title = current.title;
1610
+ const bars = this._dockPanel.tabBars();
1611
+ return find(bars, bar => bar.titles.indexOf(title) > -1) || null;
1612
+ }
1613
+
1614
+ /**
1615
+ * Handle a change to the dock area active widget.
1616
+ */
1617
+ private _onActiveChanged(
1618
+ sender: any,
1619
+ args: FocusTracker.IChangedArgs<Widget>
1620
+ ): void {
1621
+ if (args.newValue) {
1622
+ args.newValue.title.className += ` ${ACTIVE_CLASS}`;
1623
+ }
1624
+ if (args.oldValue) {
1625
+ args.oldValue.title.className = args.oldValue.title.className.replace(
1626
+ ACTIVE_CLASS,
1627
+ ''
1628
+ );
1629
+ }
1630
+ this._activeChanged.emit(args);
1631
+ }
1632
+
1633
+ /**
1634
+ * Handle a change to the dock area current widget.
1635
+ */
1636
+ private _onCurrentChanged(
1637
+ sender: any,
1638
+ args: FocusTracker.IChangedArgs<Widget>
1639
+ ): void {
1640
+ if (args.newValue) {
1641
+ args.newValue.title.className += ` ${CURRENT_CLASS}`;
1642
+ }
1643
+ if (args.oldValue) {
1644
+ args.oldValue.title.className = args.oldValue.title.className.replace(
1645
+ CURRENT_CLASS,
1646
+ ''
1647
+ );
1648
+ }
1649
+ this._currentChanged.emit(args);
1650
+ this._onLayoutModified();
1651
+ }
1652
+
1653
+ /**
1654
+ * Handle a change on the down panel widgets
1655
+ */
1656
+ private _onTabPanelChanged(): void {
1657
+ if (this._downPanel.stackedPanel.widgets.length === 0) {
1658
+ this._downPanel.hide();
1659
+ }
1660
+ this._onLayoutModified();
1661
+ }
1662
+
1663
+ /**
1664
+ * Handle a change to the layout.
1665
+ */
1666
+ private _onLayoutModified(): void {
1667
+ void this._layoutDebouncer.invoke();
1668
+ }
1669
+
1670
+ /**
1671
+ * A message hook for child add/remove messages on the main area dock panel.
1672
+ */
1673
+ private _dockChildHook = (
1674
+ handler: IMessageHandler,
1675
+ msg: Message
1676
+ ): boolean => {
1677
+ switch (msg.type) {
1678
+ case 'child-added':
1679
+ (msg as Widget.ChildMessage).child.addClass(ACTIVITY_CLASS);
1680
+ this._tracker.add((msg as Widget.ChildMessage).child);
1681
+ break;
1682
+ case 'child-removed':
1683
+ (msg as Widget.ChildMessage).child.removeClass(ACTIVITY_CLASS);
1684
+ this._tracker.remove((msg as Widget.ChildMessage).child);
1685
+ break;
1686
+ default:
1687
+ break;
1688
+ }
1689
+ return true;
1690
+ };
1691
+
1692
+ private _activeChanged = new Signal<this, ILabShell.IChangedArgs>(this);
1693
+ private _cachedLayout: DockLayout.ILayoutConfig | null = null;
1694
+ private _currentChanged = new Signal<this, ILabShell.IChangedArgs>(this);
1695
+ private _currentPath = '';
1696
+ private _currentPathChanged = new Signal<
1697
+ this,
1698
+ ILabShell.ICurrentPathChangedArgs
1699
+ >(this);
1700
+ private _modeChanged = new Signal<this, DockPanel.Mode>(this);
1701
+ private _dockPanel: DockPanel;
1702
+ private _downPanel: TabPanel;
1703
+ private _isRestored = false;
1704
+ private _layoutModified = new Signal<this, void>(this);
1705
+ private _layoutDebouncer = new Debouncer(() => {
1706
+ this._layoutModified.emit(undefined);
1707
+ }, 0);
1708
+ private _leftHandler: Private.SideBarHandler;
1709
+ private _restored = new PromiseDelegate<ILabShell.ILayout>();
1710
+ private _rightHandler: Private.SideBarHandler;
1711
+ private _tracker = new FocusTracker<Widget>();
1712
+ private _headerPanel: Panel;
1713
+ private _hsplitPanel: Private.RestorableSplitPanel;
1714
+ private _vsplitPanel: Private.RestorableSplitPanel;
1715
+ private _topHandler: Private.PanelHandler;
1716
+ private _topHandlerHiddenByUser = false;
1717
+ private _menuHandler: Private.PanelHandler;
1718
+ private _skipLinkWidget: Private.SkipLinkWidget;
1719
+ private _titleHandler: Private.TitleHandler;
1720
+ private _bottomPanel: Panel;
1721
+ private _idTypeMap = new Map<string, string>();
1722
+ private _mainOptionsCache = new Map<Widget, DocumentRegistry.IOpenOptions>();
1723
+ private _sideOptionsCache = new Map<Widget, DocumentRegistry.IOpenOptions>();
1724
+ private _userLayout: {
1725
+ 'single-document': ILabShell.IUserLayout;
1726
+ 'multiple-document': ILabShell.IUserLayout;
1727
+ };
1728
+ private _delayedWidget = new Array<ILabShell.IDelayedWidget>();
1729
+ private _translator: ITranslator;
1730
+ private _layoutRestorer: LayoutRestorer;
1731
+ }
1732
+
1733
+ namespace Private {
1734
+ /**
1735
+ * An object which holds a widget and its sort rank.
1736
+ */
1737
+ export interface IRankItem {
1738
+ /**
1739
+ * The widget for the item.
1740
+ */
1741
+ widget: Widget;
1742
+
1743
+ /**
1744
+ * The sort rank of the widget.
1745
+ */
1746
+ rank: number;
1747
+ }
1748
+
1749
+ /**
1750
+ * A less-than comparison function for side bar rank items.
1751
+ */
1752
+ export function itemCmp(first: IRankItem, second: IRankItem): number {
1753
+ return first.rank - second.rank;
1754
+ }
1755
+
1756
+ /**
1757
+ * Removes widgets that have been disposed from an area config, mutates area.
1758
+ */
1759
+ export function normalizeAreaConfig(
1760
+ parent: DockPanel,
1761
+ area?: DockLayout.AreaConfig | null
1762
+ ): void {
1763
+ if (!area) {
1764
+ return;
1765
+ }
1766
+ if (area.type === 'tab-area') {
1767
+ area.widgets = area.widgets.filter(
1768
+ widget => !widget.isDisposed && widget.parent === parent
1769
+ ) as Widget[];
1770
+ return;
1771
+ }
1772
+ area.children.forEach(child => {
1773
+ normalizeAreaConfig(parent, child);
1774
+ });
1775
+ }
1776
+
1777
+ /**
1778
+ * A class which manages a panel and sorts its widgets by rank.
1779
+ */
1780
+ export class PanelHandler {
1781
+ constructor() {
1782
+ MessageLoop.installMessageHook(this._panel, this._panelChildHook);
1783
+ }
1784
+
1785
+ /**
1786
+ * Get the panel managed by the handler.
1787
+ */
1788
+ get panel(): Panel {
1789
+ return this._panel;
1790
+ }
1791
+
1792
+ /**
1793
+ * Add a widget to the panel.
1794
+ *
1795
+ * If the widget is already added, it will be moved.
1796
+ */
1797
+ addWidget(widget: Widget, rank: number): void {
1798
+ widget.parent = null;
1799
+ const item = { widget, rank };
1800
+ const index = ArrayExt.upperBound(this._items, item, Private.itemCmp);
1801
+ ArrayExt.insert(this._items, index, item);
1802
+ this._panel.insertWidget(index, widget);
1803
+ }
1804
+
1805
+ /**
1806
+ * A message hook for child add/remove messages on the main area dock panel.
1807
+ */
1808
+ private _panelChildHook = (
1809
+ handler: IMessageHandler,
1810
+ msg: Message
1811
+ ): boolean => {
1812
+ switch (msg.type) {
1813
+ case 'child-added':
1814
+ {
1815
+ const widget = (msg as Widget.ChildMessage).child;
1816
+ // If we already know about this widget, we're done
1817
+ if (this._items.find(v => v.widget === widget)) {
1818
+ break;
1819
+ }
1820
+
1821
+ // Otherwise, add to the end by default
1822
+ const rank = this._items[this._items.length - 1].rank;
1823
+ this._items.push({ widget, rank });
1824
+ }
1825
+ break;
1826
+ case 'child-removed':
1827
+ {
1828
+ const widget = (msg as Widget.ChildMessage).child;
1829
+ ArrayExt.removeFirstWhere(this._items, v => v.widget === widget);
1830
+ }
1831
+ break;
1832
+ default:
1833
+ break;
1834
+ }
1835
+ return true;
1836
+ };
1837
+
1838
+ private _items = new Array<Private.IRankItem>();
1839
+ private _panel = new Panel();
1840
+ }
1841
+
1842
+ /**
1843
+ * A class which manages a side bar and related stacked panel.
1844
+ */
1845
+ export class SideBarHandler {
1846
+ /**
1847
+ * Construct a new side bar handler.
1848
+ */
1849
+ constructor() {
1850
+ this._sideBar = new TabBar<Widget>({
1851
+ insertBehavior: 'none',
1852
+ removeBehavior: 'none',
1853
+ allowDeselect: true,
1854
+ orientation: 'vertical'
1855
+ });
1856
+ this._stackedPanel = new StackedPanel();
1857
+ this._sideBar.hide();
1858
+ this._stackedPanel.hide();
1859
+ this._lastCurrent = null;
1860
+ this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
1861
+ this._sideBar.tabActivateRequested.connect(
1862
+ this._onTabActivateRequested,
1863
+ this
1864
+ );
1865
+ this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
1866
+ }
1867
+
1868
+ /**
1869
+ * Whether the side bar is visible
1870
+ */
1871
+ get isVisible(): boolean {
1872
+ return this._sideBar.isVisible;
1873
+ }
1874
+
1875
+ /**
1876
+ * Get the tab bar managed by the handler.
1877
+ */
1878
+ get sideBar(): TabBar<Widget> {
1879
+ return this._sideBar;
1880
+ }
1881
+
1882
+ /**
1883
+ * Get the stacked panel managed by the handler
1884
+ */
1885
+ get stackedPanel(): StackedPanel {
1886
+ return this._stackedPanel;
1887
+ }
1888
+
1889
+ /**
1890
+ * Signal fires when the stack panel or the sidebar changes
1891
+ */
1892
+ get updated(): ISignal<SideBarHandler, void> {
1893
+ return this._updated;
1894
+ }
1895
+
1896
+ /**
1897
+ * Expand the sidebar.
1898
+ *
1899
+ * #### Notes
1900
+ * This will open the most recently used tab, or the first tab
1901
+ * if there is no most recently used.
1902
+ */
1903
+ expand(): void {
1904
+ const previous =
1905
+ this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
1906
+ if (previous) {
1907
+ this.activate(previous.id);
1908
+ }
1909
+ }
1910
+
1911
+ /**
1912
+ * Activate a widget residing in the side bar by ID.
1913
+ *
1914
+ * @param id - The widget's unique ID.
1915
+ */
1916
+ activate(id: string): void {
1917
+ const widget = this._findWidgetByID(id);
1918
+ if (widget) {
1919
+ this._sideBar.currentTitle = widget.title;
1920
+ widget.activate();
1921
+ }
1922
+ }
1923
+
1924
+ /**
1925
+ * Test whether the sidebar has the given widget by id.
1926
+ */
1927
+ has(id: string): boolean {
1928
+ return this._findWidgetByID(id) !== null;
1929
+ }
1930
+
1931
+ /**
1932
+ * Collapse the sidebar so no items are expanded.
1933
+ */
1934
+ collapse(): void {
1935
+ this._sideBar.currentTitle = null;
1936
+ }
1937
+
1938
+ /**
1939
+ * Add a widget and its title to the stacked panel and side bar.
1940
+ *
1941
+ * If the widget is already added, it will be moved.
1942
+ */
1943
+ addWidget(widget: Widget, rank: number): void {
1944
+ widget.parent = null;
1945
+ widget.hide();
1946
+ const item = { widget, rank };
1947
+ const index = this._findInsertIndex(item);
1948
+ ArrayExt.insert(this._items, index, item);
1949
+ this._stackedPanel.insertWidget(index, widget);
1950
+ const title = this._sideBar.insertTab(index, widget.title);
1951
+ // Store the parent id in the title dataset
1952
+ // in order to dispatch click events to the right widget.
1953
+ title.dataset = { id: widget.id };
1954
+
1955
+ if (title.icon instanceof LabIcon) {
1956
+ // bind an appropriate style to the icon
1957
+ title.icon = title.icon.bindprops({
1958
+ stylesheet: 'sideBar'
1959
+ });
1960
+ } else if (typeof title.icon === 'string' && title.icon != '') {
1961
+ // add some classes to help with displaying css background imgs
1962
+ title.iconClass = classes(title.iconClass, 'jp-Icon', 'jp-Icon-20');
1963
+ } else if (!title.icon && !title.label) {
1964
+ // add a fallback icon if there is no title label nor icon
1965
+ title.icon = tabIcon.bindprops({
1966
+ stylesheet: 'sideBar'
1967
+ });
1968
+ }
1969
+
1970
+ this._refreshVisibility();
1971
+ }
1972
+
1973
+ /**
1974
+ * Dehydrate the side bar data.
1975
+ */
1976
+ dehydrate(): ILabShell.ISideArea {
1977
+ const collapsed = this._sideBar.currentTitle === null;
1978
+ const widgets = Array.from(this._stackedPanel.widgets);
1979
+ const currentWidget = widgets[this._sideBar.currentIndex];
1980
+ return {
1981
+ collapsed,
1982
+ currentWidget,
1983
+ visible: !this._isHiddenByUser,
1984
+ widgets
1985
+ };
1986
+ }
1987
+
1988
+ /**
1989
+ * Rehydrate the side bar.
1990
+ */
1991
+ rehydrate(data: ILabShell.ISideArea): void {
1992
+ if (data.currentWidget) {
1993
+ this.activate(data.currentWidget.id);
1994
+ }
1995
+ if (data.collapsed) {
1996
+ this.collapse();
1997
+ }
1998
+ if (!data.visible) {
1999
+ this.hide();
2000
+ }
2001
+ }
2002
+
2003
+ /**
2004
+ * Hide the side bar even if it contains widgets
2005
+ */
2006
+ hide(): void {
2007
+ this._isHiddenByUser = true;
2008
+ this._refreshVisibility();
2009
+ }
2010
+
2011
+ /**
2012
+ * Show the side bar if it contains widgets
2013
+ */
2014
+ show(): void {
2015
+ this._isHiddenByUser = false;
2016
+ this._refreshVisibility();
2017
+ }
2018
+
2019
+ /**
2020
+ * Find the insertion index for a rank item.
2021
+ */
2022
+ private _findInsertIndex(item: Private.IRankItem): number {
2023
+ return ArrayExt.upperBound(this._items, item, Private.itemCmp);
2024
+ }
2025
+
2026
+ /**
2027
+ * Find the index of the item with the given widget, or `-1`.
2028
+ */
2029
+ private _findWidgetIndex(widget: Widget): number {
2030
+ return ArrayExt.findFirstIndex(this._items, i => i.widget === widget);
2031
+ }
2032
+
2033
+ /**
2034
+ * Find the widget which owns the given title, or `null`.
2035
+ */
2036
+ private _findWidgetByTitle(title: Title<Widget>): Widget | null {
2037
+ const item = find(this._items, value => value.widget.title === title);
2038
+ return item ? item.widget : null;
2039
+ }
2040
+
2041
+ /**
2042
+ * Find the widget with the given id, or `null`.
2043
+ */
2044
+ private _findWidgetByID(id: string): Widget | null {
2045
+ const item = find(this._items, value => value.widget.id === id);
2046
+ return item ? item.widget : null;
2047
+ }
2048
+
2049
+ /**
2050
+ * Refresh the visibility of the side bar and stacked panel.
2051
+ */
2052
+ private _refreshVisibility(): void {
2053
+ this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
2054
+ this._sideBar.setHidden(
2055
+ this._isHiddenByUser || this._sideBar.titles.length === 0
2056
+ );
2057
+ this._updated.emit();
2058
+ }
2059
+
2060
+ /**
2061
+ * Handle the `currentChanged` signal from the sidebar.
2062
+ */
2063
+ private _onCurrentChanged(
2064
+ sender: TabBar<Widget>,
2065
+ args: TabBar.ICurrentChangedArgs<Widget>
2066
+ ): void {
2067
+ const oldWidget = args.previousTitle
2068
+ ? this._findWidgetByTitle(args.previousTitle)
2069
+ : null;
2070
+ const newWidget = args.currentTitle
2071
+ ? this._findWidgetByTitle(args.currentTitle)
2072
+ : null;
2073
+ if (oldWidget) {
2074
+ oldWidget.hide();
2075
+ }
2076
+ if (newWidget) {
2077
+ newWidget.show();
2078
+ }
2079
+ this._lastCurrent = newWidget || oldWidget;
2080
+ this._refreshVisibility();
2081
+ }
2082
+
2083
+ /**
2084
+ * Handle a `tabActivateRequest` signal from the sidebar.
2085
+ */
2086
+ private _onTabActivateRequested(
2087
+ sender: TabBar<Widget>,
2088
+ args: TabBar.ITabActivateRequestedArgs<Widget>
2089
+ ): void {
2090
+ args.title.owner.activate();
2091
+ }
2092
+
2093
+ /*
2094
+ * Handle the `widgetRemoved` signal from the stacked panel.
2095
+ */
2096
+ private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
2097
+ if (widget === this._lastCurrent) {
2098
+ this._lastCurrent = null;
2099
+ }
2100
+ ArrayExt.removeAt(this._items, this._findWidgetIndex(widget));
2101
+ this._sideBar.removeTab(widget.title);
2102
+ this._refreshVisibility();
2103
+ }
2104
+
2105
+ private _isHiddenByUser = false;
2106
+ private _items = new Array<Private.IRankItem>();
2107
+ private _sideBar: TabBar<Widget>;
2108
+ private _stackedPanel: StackedPanel;
2109
+ private _lastCurrent: Widget | null;
2110
+ private _updated: Signal<SideBarHandler, void> = new Signal(this);
2111
+ }
2112
+
2113
+ export class SkipLinkWidget extends Widget {
2114
+ /**
2115
+ * Construct a new skipLink widget.
2116
+ */
2117
+ constructor(shell: ILabShell) {
2118
+ super();
2119
+ this.addClass('jp-skiplink');
2120
+ this.id = 'jp-skiplink';
2121
+ this._shell = shell;
2122
+ this._createSkipLink('Skip to left side bar');
2123
+ }
2124
+
2125
+ handleEvent(event: Event): void {
2126
+ switch (event.type) {
2127
+ case 'click':
2128
+ this._focusLeftSideBar();
2129
+ break;
2130
+ }
2131
+ }
2132
+
2133
+ /**
2134
+ * Handle `after-attach` messages for the widget.
2135
+ */
2136
+ protected onAfterAttach(msg: Message): void {
2137
+ super.onAfterAttach(msg);
2138
+ this.node.addEventListener('click', this);
2139
+ }
2140
+
2141
+ /**
2142
+ * A message handler invoked on a `'before-detach'`
2143
+ * message
2144
+ */
2145
+ protected onBeforeDetach(msg: Message): void {
2146
+ this.node.removeEventListener('click', this);
2147
+ super.onBeforeDetach(msg);
2148
+ }
2149
+
2150
+ private _focusLeftSideBar() {
2151
+ this._shell.expandLeft();
2152
+ }
2153
+ private _shell: ILabShell;
2154
+
2155
+ private _createSkipLink(skipLinkText: string): void {
2156
+ const skipLink = document.createElement('a');
2157
+ skipLink.href = '#';
2158
+ skipLink.tabIndex = 1;
2159
+ skipLink.text = skipLinkText;
2160
+ skipLink.className = 'skip-link';
2161
+ this.node.appendChild(skipLink);
2162
+ }
2163
+ }
2164
+
2165
+ export class TitleHandler extends Widget {
2166
+ /**
2167
+ * Construct a new title handler.
2168
+ */
2169
+ constructor(shell: ILabShell) {
2170
+ super();
2171
+ const inputElement = document.createElement('input');
2172
+ inputElement.type = 'text';
2173
+ this.node.appendChild(inputElement);
2174
+ this._shell = shell;
2175
+ this.id = 'jp-title-panel-title';
2176
+ }
2177
+
2178
+ /**
2179
+ * Handle `after-attach` messages for the widget.
2180
+ */
2181
+ protected onAfterAttach(msg: Message): void {
2182
+ super.onAfterAttach(msg);
2183
+ this.inputElement.addEventListener('keyup', this);
2184
+ this.inputElement.addEventListener('click', this);
2185
+ this.inputElement.addEventListener('blur', this);
2186
+ }
2187
+
2188
+ /**
2189
+ * Handle `before-detach` messages for the widget.
2190
+ */
2191
+ protected onBeforeDetach(msg: Message): void {
2192
+ super.onBeforeDetach(msg);
2193
+ this.inputElement.removeEventListener('keyup', this);
2194
+ this.inputElement.removeEventListener('click', this);
2195
+ this.inputElement.removeEventListener('blur', this);
2196
+ }
2197
+
2198
+ handleEvent(event: Event): void {
2199
+ switch (event.type) {
2200
+ case 'keyup':
2201
+ void this._evtKeyUp(event as KeyboardEvent);
2202
+ break;
2203
+ case 'click':
2204
+ this._evtClick(event as MouseEvent);
2205
+ break;
2206
+ case 'blur':
2207
+ this._selected = false;
2208
+ break;
2209
+ }
2210
+ }
2211
+
2212
+ /**
2213
+ * Handle `keyup` events on the handler.
2214
+ */
2215
+ private async _evtKeyUp(event: KeyboardEvent): Promise<void> {
2216
+ if (event.key == 'Enter') {
2217
+ const widget = this._shell.currentWidget;
2218
+ if (widget == null) {
2219
+ return;
2220
+ }
2221
+ const oldName = widget.title.label;
2222
+ const inputElement = this.inputElement;
2223
+ const newName = inputElement.value;
2224
+ inputElement.blur();
2225
+
2226
+ if (newName !== oldName) {
2227
+ widget.title.label = newName;
2228
+ } else {
2229
+ inputElement.value = oldName;
2230
+ }
2231
+ }
2232
+ }
2233
+
2234
+ /**
2235
+ * Handle `click` events on the handler.
2236
+ */
2237
+ private _evtClick(event: MouseEvent): void {
2238
+ // only handle primary button clicks
2239
+ if (event.button !== 0 || this._selected) {
2240
+ return;
2241
+ }
2242
+
2243
+ const inputElement = this.inputElement;
2244
+
2245
+ event.preventDefault();
2246
+ event.stopPropagation();
2247
+
2248
+ this._selected = true;
2249
+
2250
+ const selectEnd = inputElement.value.indexOf('.');
2251
+ if (selectEnd === -1) {
2252
+ inputElement.select();
2253
+ } else {
2254
+ inputElement.setSelectionRange(0, selectEnd);
2255
+ }
2256
+ }
2257
+
2258
+ /**
2259
+ * The input element containing the parent widget's title.
2260
+ */
2261
+ get inputElement(): HTMLInputElement {
2262
+ return this.node.children[0] as HTMLInputElement;
2263
+ }
2264
+
2265
+ private _shell: ILabShell;
2266
+ private _selected: boolean = false;
2267
+ }
2268
+
2269
+ export class RestorableSplitPanel extends SplitPanel {
2270
+ updated: Signal<RestorableSplitPanel, void>;
2271
+
2272
+ constructor(options: SplitPanel.IOptions = {}) {
2273
+ super(options);
2274
+ this.updated = new Signal(this);
2275
+ }
2276
+
2277
+ /**
2278
+ * Emit 'updated' signal on 'update' requests.
2279
+ */
2280
+ protected onUpdateRequest(msg: Message): void {
2281
+ super.onUpdateRequest(msg);
2282
+ this.updated.emit();
2283
+ }
2284
+ }
2285
+ }