UNPKG

53.2 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { DocumentWidget } from '@jupyterlab/docregistry';
4import { nullTranslator } from '@jupyterlab/translation';
5import { classes, DockPanelSvg, LabIcon, TabPanelSvg } from '@jupyterlab/ui-components';
6import { ArrayExt, find, iter, toArray } from '@lumino/algorithm';
7import { PromiseDelegate, Token } from '@lumino/coreutils';
8import { MessageLoop } from '@lumino/messaging';
9import { Debouncer } from '@lumino/polling';
10import { Signal } from '@lumino/signaling';
11import { BoxLayout, BoxPanel, FocusTracker, Panel, SplitPanel, StackedPanel, TabBar, Widget } from '@lumino/widgets';
12/**
13 * The class name added to AppShell instances.
14 */
15const APPLICATION_SHELL_CLASS = 'jp-LabShell';
16/**
17 * The class name added to side bar instances.
18 */
19const SIDEBAR_CLASS = 'jp-SideBar';
20/**
21 * The class name added to the current widget's title.
22 */
23const CURRENT_CLASS = 'jp-mod-current';
24/**
25 * The class name added to the active widget's title.
26 */
27const ACTIVE_CLASS = 'jp-mod-active';
28/**
29 * The default rank of items added to a sidebar.
30 */
31const DEFAULT_RANK = 900;
32const ACTIVITY_CLASS = 'jp-Activity';
33/**
34 * The JupyterLab application shell token.
35 */
36export const ILabShell = new Token('@jupyterlab/application:ILabShell');
37/**
38 * The application shell for JupyterLab.
39 */
40export class LabShell extends Widget {
41 /**
42 * Construct a new application shell.
43 */
44 constructor(options) {
45 super();
46 /**
47 * A message hook for child add/remove messages on the main area dock panel.
48 */
49 this._dockChildHook = (handler, msg) => {
50 switch (msg.type) {
51 case 'child-added':
52 msg.child.addClass(ACTIVITY_CLASS);
53 this._tracker.add(msg.child);
54 break;
55 case 'child-removed':
56 msg.child.removeClass(ACTIVITY_CLASS);
57 this._tracker.remove(msg.child);
58 break;
59 default:
60 break;
61 }
62 return true;
63 };
64 this._activeChanged = new Signal(this);
65 this._cachedLayout = null;
66 this._currentChanged = new Signal(this);
67 this._currentPath = '';
68 this._currentPathChanged = new Signal(this);
69 this._modeChanged = new Signal(this);
70 this._isRestored = false;
71 this._layoutModified = new Signal(this);
72 this._layoutDebouncer = new Debouncer(() => {
73 this._layoutModified.emit(undefined);
74 }, 0);
75 this._restored = new PromiseDelegate();
76 this._tracker = new FocusTracker();
77 this._mainOptionsCache = new Map();
78 this._sideOptionsCache = new Map();
79 this.addClass(APPLICATION_SHELL_CLASS);
80 this.id = 'main';
81 const trans = ((options && options.translator) || nullTranslator).load('jupyterlab');
82 const headerPanel = (this._headerPanel = new BoxPanel());
83 const menuHandler = (this._menuHandler = new Private.PanelHandler());
84 menuHandler.panel.node.setAttribute('role', 'navigation');
85 menuHandler.panel.node.setAttribute('aria-label', trans.__('main'));
86 const topHandler = (this._topHandler = new Private.PanelHandler());
87 topHandler.panel.node.setAttribute('role', 'banner');
88 const bottomPanel = (this._bottomPanel = new BoxPanel());
89 bottomPanel.node.setAttribute('role', 'contentinfo');
90 const hboxPanel = new BoxPanel();
91 const vsplitPanel = (this._vsplitPanel = new Private.RestorableSplitPanel());
92 const dockPanel = (this._dockPanel = new DockPanelSvg({
93 hiddenMode: Widget.HiddenMode.Scale,
94 translator: options === null || options === void 0 ? void 0 : options.translator
95 }));
96 MessageLoop.installMessageHook(dockPanel, this._dockChildHook);
97 const hsplitPanel = (this._hsplitPanel = new Private.RestorableSplitPanel());
98 const downPanel = (this._downPanel = new TabPanelSvg({
99 tabsMovable: true
100 }));
101 const leftHandler = (this._leftHandler = new Private.SideBarHandler());
102 const rightHandler = (this._rightHandler = new Private.SideBarHandler());
103 const rootLayout = new BoxLayout();
104 headerPanel.id = 'jp-header-panel';
105 menuHandler.panel.id = 'jp-menu-panel';
106 topHandler.panel.id = 'jp-top-panel';
107 bottomPanel.id = 'jp-bottom-panel';
108 hboxPanel.id = 'jp-main-content-panel';
109 vsplitPanel.id = 'jp-main-vsplit-panel';
110 dockPanel.id = 'jp-main-dock-panel';
111 hsplitPanel.id = 'jp-main-split-panel';
112 downPanel.id = 'jp-down-stack';
113 leftHandler.sideBar.addClass(SIDEBAR_CLASS);
114 leftHandler.sideBar.addClass('jp-mod-left');
115 leftHandler.sideBar.node.setAttribute('aria-label', trans.__('main sidebar'));
116 leftHandler.sideBar.contentNode.setAttribute('aria-label', trans.__('main sidebar'));
117 leftHandler.sideBar.node.setAttribute('role', 'complementary');
118 leftHandler.stackedPanel.id = 'jp-left-stack';
119 rightHandler.sideBar.addClass(SIDEBAR_CLASS);
120 rightHandler.sideBar.addClass('jp-mod-right');
121 rightHandler.sideBar.node.setAttribute('aria-label', trans.__('alternate sidebar'));
122 rightHandler.sideBar.contentNode.setAttribute('aria-label', trans.__('alternate sidebar'));
123 rightHandler.sideBar.node.setAttribute('role', 'complementary');
124 rightHandler.stackedPanel.id = 'jp-right-stack';
125 dockPanel.node.setAttribute('role', 'main');
126 hboxPanel.spacing = 0;
127 vsplitPanel.spacing = 1;
128 dockPanel.spacing = 5;
129 hsplitPanel.spacing = 1;
130 headerPanel.direction = 'top-to-bottom';
131 vsplitPanel.orientation = 'vertical';
132 hboxPanel.direction = 'left-to-right';
133 hsplitPanel.orientation = 'horizontal';
134 bottomPanel.direction = 'bottom-to-top';
135 SplitPanel.setStretch(leftHandler.stackedPanel, 0);
136 SplitPanel.setStretch(downPanel, 0);
137 SplitPanel.setStretch(dockPanel, 1);
138 SplitPanel.setStretch(rightHandler.stackedPanel, 0);
139 BoxPanel.setStretch(leftHandler.sideBar, 0);
140 BoxPanel.setStretch(hsplitPanel, 1);
141 BoxPanel.setStretch(rightHandler.sideBar, 0);
142 SplitPanel.setStretch(vsplitPanel, 1);
143 hsplitPanel.addWidget(leftHandler.stackedPanel);
144 hsplitPanel.addWidget(dockPanel);
145 hsplitPanel.addWidget(rightHandler.stackedPanel);
146 vsplitPanel.addWidget(hsplitPanel);
147 vsplitPanel.addWidget(downPanel);
148 hboxPanel.addWidget(leftHandler.sideBar);
149 hboxPanel.addWidget(vsplitPanel);
150 hboxPanel.addWidget(rightHandler.sideBar);
151 rootLayout.direction = 'top-to-bottom';
152 rootLayout.spacing = 0; // TODO make this configurable?
153 // Use relative sizing to set the width of the side panels.
154 // This will still respect the min-size of children widget in the stacked
155 // panel. The default sizes will be overwritten during layout restoration.
156 vsplitPanel.setRelativeSizes([3, 1]);
157 hsplitPanel.setRelativeSizes([1, 2.5, 1]);
158 BoxLayout.setStretch(headerPanel, 0);
159 BoxLayout.setStretch(menuHandler.panel, 0);
160 BoxLayout.setStretch(topHandler.panel, 0);
161 BoxLayout.setStretch(hboxPanel, 1);
162 BoxLayout.setStretch(bottomPanel, 0);
163 rootLayout.addWidget(headerPanel);
164 rootLayout.addWidget(topHandler.panel);
165 rootLayout.addWidget(hboxPanel);
166 rootLayout.addWidget(bottomPanel);
167 // initially hiding header and bottom panel when no elements inside,
168 this._headerPanel.hide();
169 this._bottomPanel.hide();
170 this._downPanel.hide();
171 this.layout = rootLayout;
172 // Connect change listeners.
173 this._tracker.currentChanged.connect(this._onCurrentChanged, this);
174 this._tracker.activeChanged.connect(this._onActiveChanged, this);
175 // Connect main layout change listener.
176 this._dockPanel.layoutModified.connect(this._onLayoutModified, this);
177 // Connect vsplit layout change listener
178 this._vsplitPanel.updated.connect(this._onLayoutModified, this);
179 // Connect down panel change listeners
180 this._downPanel.currentChanged.connect(this._onLayoutModified, this);
181 this._downPanel.tabBar.tabMoved.connect(this._onTabPanelChanged, this);
182 this._downPanel.stackedPanel.widgetRemoved.connect(this._onTabPanelChanged, this);
183 // Catch current changed events on the side handlers.
184 this._leftHandler.sideBar.currentChanged.connect(this._onLayoutModified, this);
185 this._rightHandler.sideBar.currentChanged.connect(this._onLayoutModified, this);
186 // Catch update events on the horizontal split panel
187 this._hsplitPanel.updated.connect(this._onLayoutModified, this);
188 // Setup single-document-mode title bar
189 const titleHandler = (this._titleHandler = new Private.TitleHandler(this));
190 this.add(titleHandler, 'top', { rank: 100 });
191 if (this._dockPanel.mode === 'multiple-document') {
192 this._topHandler.addWidget(this._menuHandler.panel, 100);
193 titleHandler.hide();
194 }
195 else {
196 rootLayout.insertWidget(2, this._menuHandler.panel);
197 }
198 // Skip Links
199 const skipLinkWidget = (this._skipLinkWidget = new Private.SkipLinkWidget(this));
200 this.add(skipLinkWidget, 'top', { rank: 0 });
201 this._skipLinkWidget.show();
202 // Wire up signals to update the title panel of the simple interface mode to
203 // follow the title of this.currentWidget
204 this.currentChanged.connect((sender, args) => {
205 let newValue = args.newValue;
206 let oldValue = args.oldValue;
207 // Stop watching the title of the previously current widget
208 if (oldValue) {
209 oldValue.title.changed.disconnect(this._updateTitlePanelTitle, this);
210 }
211 // Start watching the title of the new current widget
212 if (newValue) {
213 newValue.title.changed.connect(this._updateTitlePanelTitle, this);
214 this._updateTitlePanelTitle();
215 }
216 if (newValue && newValue instanceof DocumentWidget) {
217 newValue.context.pathChanged.connect(this._updateCurrentPath, this);
218 }
219 this._updateCurrentPath();
220 });
221 }
222 /**
223 * A signal emitted when main area's active focus changes.
224 */
225 get activeChanged() {
226 return this._activeChanged;
227 }
228 /**
229 * The active widget in the shell's main area.
230 */
231 get activeWidget() {
232 return this._tracker.activeWidget;
233 }
234 /**
235 * A signal emitted when main area's current focus changes.
236 */
237 get currentChanged() {
238 return this._currentChanged;
239 }
240 /**
241 * A signal emitted when the shell/dock panel change modes (single/multiple document).
242 */
243 get modeChanged() {
244 return this._modeChanged;
245 }
246 /**
247 * A signal emitted when the path of the current document changes.
248 *
249 * This also fires when the current document itself changes.
250 */
251 get currentPathChanged() {
252 return this._currentPathChanged;
253 }
254 /**
255 * The current widget in the shell's main area.
256 */
257 get currentWidget() {
258 return this._tracker.currentWidget;
259 }
260 /**
261 * A signal emitted when the main area's layout is modified.
262 */
263 get layoutModified() {
264 return this._layoutModified;
265 }
266 /**
267 * Whether the left area is collapsed.
268 */
269 get leftCollapsed() {
270 return !this._leftHandler.sideBar.currentTitle;
271 }
272 /**
273 * Whether the left area is collapsed.
274 */
275 get rightCollapsed() {
276 return !this._rightHandler.sideBar.currentTitle;
277 }
278 /**
279 * Whether JupyterLab is in presentation mode with the
280 * `jp-mod-presentationMode` CSS class.
281 */
282 get presentationMode() {
283 return this.hasClass('jp-mod-presentationMode');
284 }
285 /**
286 * Enable/disable presentation mode (`jp-mod-presentationMode` CSS class) with
287 * a boolean.
288 */
289 set presentationMode(value) {
290 this.toggleClass('jp-mod-presentationMode', value);
291 }
292 /**
293 * The main dock area's user interface mode.
294 */
295 get mode() {
296 return this._dockPanel.mode;
297 }
298 set mode(mode) {
299 const dock = this._dockPanel;
300 if (mode === dock.mode) {
301 return;
302 }
303 const applicationCurrentWidget = this.currentWidget;
304 if (mode === 'single-document') {
305 // Cache the current multi-document layout before changing the mode.
306 this._cachedLayout = dock.saveLayout();
307 dock.mode = mode;
308 // In case the active widget in the dock panel is *not* the active widget
309 // of the application, defer to the application.
310 if (this.currentWidget) {
311 dock.activateWidget(this.currentWidget);
312 }
313 // Adjust menu and title
314 this.layout.insertWidget(2, this._menuHandler.panel);
315 this._titleHandler.show();
316 this._updateTitlePanelTitle();
317 }
318 else {
319 // Cache a reference to every widget currently in the dock panel before
320 // changing its mode.
321 const widgets = toArray(dock.widgets());
322 dock.mode = mode;
323 // Restore the original layout.
324 if (this._cachedLayout) {
325 // Remove any disposed widgets in the cached layout and restore.
326 Private.normalizeAreaConfig(dock, this._cachedLayout.main);
327 dock.restoreLayout(this._cachedLayout);
328 this._cachedLayout = null;
329 }
330 // Add any widgets created during single document mode, which have
331 // subsequently been removed from the dock panel after the multiple document
332 // layout has been restored. If the widget has add options cached for
333 // the widget (i.e., if it has been placed with respect to another widget),
334 // then take that into account.
335 widgets.forEach(widget => {
336 if (!widget.parent) {
337 this._addToMainArea(widget, Object.assign(Object.assign({}, this._mainOptionsCache.get(widget)), { activate: false }));
338 }
339 });
340 this._mainOptionsCache.clear();
341 // In case the active widget in the dock panel is *not* the active widget
342 // of the application, defer to the application.
343 if (applicationCurrentWidget) {
344 dock.activateWidget(applicationCurrentWidget);
345 }
346 // Adjust menu and title
347 this.add(this._menuHandler.panel, 'top', { rank: 100 });
348 // this._topHandler.addWidget(this._menuHandler.panel, 100)
349 this._titleHandler.hide();
350 }
351 // Set the mode data attribute on the applications shell node.
352 this.node.dataset.shellMode = mode;
353 this._downPanel.fit();
354 // Emit the mode changed signal
355 this._modeChanged.emit(mode);
356 }
357 /**
358 * Promise that resolves when state is first restored, returning layout
359 * description.
360 */
361 get restored() {
362 return this._restored.promise;
363 }
364 /**
365 * Activate a widget in its area.
366 */
367 activateById(id) {
368 if (this._leftHandler.has(id)) {
369 this._leftHandler.activate(id);
370 return;
371 }
372 if (this._rightHandler.has(id)) {
373 this._rightHandler.activate(id);
374 return;
375 }
376 const tabIndex = this._downPanel.tabBar.titles.findIndex(title => title.owner.id === id);
377 if (tabIndex >= 0) {
378 this._downPanel.currentIndex = tabIndex;
379 return;
380 }
381 const dock = this._dockPanel;
382 const widget = find(dock.widgets(), value => value.id === id);
383 if (widget) {
384 dock.activateWidget(widget);
385 }
386 }
387 /*
388 * Activate the next Tab in the active TabBar.
389 */
390 activateNextTab() {
391 const current = this._currentTabBar();
392 if (!current) {
393 return;
394 }
395 const ci = current.currentIndex;
396 if (ci === -1) {
397 return;
398 }
399 if (ci < current.titles.length - 1) {
400 current.currentIndex += 1;
401 if (current.currentTitle) {
402 current.currentTitle.owner.activate();
403 }
404 return;
405 }
406 if (ci === current.titles.length - 1) {
407 const nextBar = this._adjacentBar('next');
408 if (nextBar) {
409 nextBar.currentIndex = 0;
410 if (nextBar.currentTitle) {
411 nextBar.currentTitle.owner.activate();
412 }
413 }
414 }
415 }
416 /*
417 * Whether the add buttons for each main area tab bar are enabled.
418 */
419 get addButtonEnabled() {
420 return this._dockPanel.addButtonEnabled;
421 }
422 set addButtonEnabled(value) {
423 this._dockPanel.addButtonEnabled = value;
424 }
425 /*
426 * A signal emitted when the add button on a main area tab bar is clicked.
427 */
428 get addRequested() {
429 return this._dockPanel.addRequested;
430 }
431 /*
432 * Activate the previous Tab in the active TabBar.
433 */
434 activatePreviousTab() {
435 const current = this._currentTabBar();
436 if (!current) {
437 return;
438 }
439 const ci = current.currentIndex;
440 if (ci === -1) {
441 return;
442 }
443 if (ci > 0) {
444 current.currentIndex -= 1;
445 if (current.currentTitle) {
446 current.currentTitle.owner.activate();
447 }
448 return;
449 }
450 if (ci === 0) {
451 const prevBar = this._adjacentBar('previous');
452 if (prevBar) {
453 const len = prevBar.titles.length;
454 prevBar.currentIndex = len - 1;
455 if (prevBar.currentTitle) {
456 prevBar.currentTitle.owner.activate();
457 }
458 }
459 }
460 }
461 /*
462 * Activate the next TabBar.
463 */
464 activateNextTabBar() {
465 const nextBar = this._adjacentBar('next');
466 if (nextBar) {
467 if (nextBar.currentTitle) {
468 nextBar.currentTitle.owner.activate();
469 }
470 }
471 }
472 /*
473 * Activate the next TabBar.
474 */
475 activatePreviousTabBar() {
476 const nextBar = this._adjacentBar('previous');
477 if (nextBar) {
478 if (nextBar.currentTitle) {
479 nextBar.currentTitle.owner.activate();
480 }
481 }
482 }
483 add(widget, area = 'main', options) {
484 switch (area || 'main') {
485 case 'bottom':
486 return this._addToBottomArea(widget, options);
487 case 'down':
488 return this._addToDownArea(widget, options);
489 case 'header':
490 return this._addToHeaderArea(widget, options);
491 case 'left':
492 return this._addToLeftArea(widget, options);
493 case 'main':
494 return this._addToMainArea(widget, options);
495 case 'menu':
496 return this._addToMenuArea(widget, options);
497 case 'right':
498 return this._addToRightArea(widget, options);
499 case 'top':
500 return this._addToTopArea(widget, options);
501 default:
502 throw new Error(`Invalid area: ${area}`);
503 }
504 }
505 /**
506 * Collapse the left area.
507 */
508 collapseLeft() {
509 this._leftHandler.collapse();
510 this._onLayoutModified();
511 }
512 /**
513 * Collapse the right area.
514 */
515 collapseRight() {
516 this._rightHandler.collapse();
517 this._onLayoutModified();
518 }
519 /**
520 * Dispose the shell.
521 */
522 dispose() {
523 if (this.isDisposed) {
524 return;
525 }
526 this._layoutDebouncer.dispose();
527 super.dispose();
528 }
529 /**
530 * Expand the left area.
531 *
532 * #### Notes
533 * This will open the most recently used tab,
534 * or the first tab if there is no most recently used.
535 */
536 expandLeft() {
537 this._leftHandler.expand();
538 this._onLayoutModified();
539 }
540 /**
541 * Expand the right area.
542 *
543 * #### Notes
544 * This will open the most recently used tab,
545 * or the first tab if there is no most recently used.
546 */
547 expandRight() {
548 this._rightHandler.expand();
549 this._onLayoutModified();
550 }
551 /**
552 * Close all widgets in the main and down area.
553 */
554 closeAll() {
555 // Make a copy of all the widget in the dock panel (using `toArray()`)
556 // before removing them because removing them while iterating through them
557 // modifies the underlying data of the iterator.
558 toArray(this._dockPanel.widgets()).forEach(widget => widget.close());
559 this._downPanel.stackedPanel.widgets.forEach(widget => widget.close());
560 }
561 /**
562 * True if the given area is empty.
563 */
564 isEmpty(area) {
565 switch (area) {
566 case 'bottom':
567 return this._bottomPanel.widgets.length === 0;
568 case 'down':
569 return this._downPanel.stackedPanel.widgets.length === 0;
570 case 'header':
571 return this._headerPanel.widgets.length === 0;
572 case 'left':
573 return this._leftHandler.stackedPanel.widgets.length === 0;
574 case 'main':
575 return this._dockPanel.isEmpty;
576 case 'menu':
577 return this._menuHandler.panel.widgets.length === 0;
578 case 'right':
579 return this._rightHandler.stackedPanel.widgets.length === 0;
580 case 'top':
581 return this._topHandler.panel.widgets.length === 0;
582 default:
583 return true;
584 }
585 }
586 /**
587 * Restore the layout state for the application shell.
588 */
589 restoreLayout(mode, layout) {
590 var _a, _b;
591 const { mainArea, downArea, leftArea, rightArea, relativeSizes } = layout;
592 // Rehydrate the main area.
593 if (mainArea) {
594 const { currentWidget, dock } = mainArea;
595 if (dock) {
596 this._dockPanel.restoreLayout(dock);
597 }
598 if (mode) {
599 this.mode = mode;
600 }
601 if (currentWidget) {
602 this.activateById(currentWidget.id);
603 }
604 }
605 else {
606 // This is needed when loading in an empty workspace in single doc mode
607 if (mode) {
608 this.mode = mode;
609 }
610 }
611 // Rehydrate the down area
612 if (downArea) {
613 const { currentWidget, widgets, size } = downArea;
614 const widgetIds = (_a = widgets === null || widgets === void 0 ? void 0 : widgets.map(widget => widget.id)) !== null && _a !== void 0 ? _a : [];
615 // Remove absent widgets
616 this._downPanel.tabBar.titles
617 .filter(title => !widgetIds.includes(title.owner.id))
618 .map(title => title.owner.close());
619 // Add new widgets
620 const titleIds = this._downPanel.tabBar.titles.map(title => title.owner.id);
621 widgets === null || widgets === void 0 ? void 0 : widgets.filter(widget => !titleIds.includes(widget.id)).map(widget => this._downPanel.addWidget(widget));
622 // Reorder tabs
623 while (!ArrayExt.shallowEqual(widgetIds, this._downPanel.tabBar.titles.map(title => title.owner.id))) {
624 this._downPanel.tabBar.titles.forEach((title, index) => {
625 const position = widgetIds.findIndex(id => title.owner.id == id);
626 if (position >= 0 && position != index) {
627 this._downPanel.tabBar.insertTab(position, title);
628 }
629 });
630 }
631 if (currentWidget) {
632 const index = this._downPanel.stackedPanel.widgets.findIndex(widget => widget.id === currentWidget.id);
633 if (index) {
634 this._downPanel.currentIndex = index;
635 (_b = this._downPanel.currentWidget) === null || _b === void 0 ? void 0 : _b.activate();
636 }
637 }
638 if (size && size > 0.0) {
639 this._vsplitPanel.setRelativeSizes([1.0 - size, size]);
640 }
641 else {
642 // Close all tabs and hide the panel
643 this._downPanel.stackedPanel.widgets.forEach(widget => widget.close());
644 this._downPanel.hide();
645 }
646 }
647 // Rehydrate the left area.
648 if (leftArea) {
649 this._leftHandler.rehydrate(leftArea);
650 }
651 else {
652 if (mode === 'single-document') {
653 this.collapseLeft();
654 }
655 }
656 // Rehydrate the right area.
657 if (rightArea) {
658 this._rightHandler.rehydrate(rightArea);
659 }
660 else {
661 if (mode === 'single-document') {
662 this.collapseRight();
663 }
664 }
665 // Restore the relative sizes.
666 if (relativeSizes) {
667 this._hsplitPanel.setRelativeSizes(relativeSizes);
668 }
669 if (!this._isRestored) {
670 // Make sure all messages in the queue are finished before notifying
671 // any extensions that are waiting for the promise that guarantees the
672 // application state has been restored.
673 MessageLoop.flush();
674 this._restored.resolve(layout);
675 }
676 }
677 /**
678 * Save the dehydrated state of the application shell.
679 */
680 saveLayout() {
681 // If the application is in single document mode, use the cached layout if
682 // available. Otherwise, default to querying the dock panel for layout.
683 const layout = {
684 mainArea: {
685 currentWidget: this._tracker.currentWidget,
686 dock: this.mode === 'single-document'
687 ? this._cachedLayout || this._dockPanel.saveLayout()
688 : this._dockPanel.saveLayout()
689 },
690 downArea: {
691 currentWidget: this._downPanel.currentWidget,
692 widgets: toArray(this._downPanel.stackedPanel.widgets),
693 size: this._vsplitPanel.relativeSizes()[1]
694 },
695 leftArea: this._leftHandler.dehydrate(),
696 rightArea: this._rightHandler.dehydrate(),
697 relativeSizes: this._hsplitPanel.relativeSizes()
698 };
699 return layout;
700 }
701 /**
702 * Update the shell configuration.
703 *
704 * @param config Shell configuration
705 */
706 updateConfig(config) {
707 if (config.hiddenMode) {
708 this._dockPanel.hiddenMode =
709 config.hiddenMode === 'display'
710 ? Widget.HiddenMode.Display
711 : Widget.HiddenMode.Scale;
712 }
713 }
714 /**
715 * Returns the widgets for an application area.
716 */
717 widgets(area) {
718 switch (area !== null && area !== void 0 ? area : 'main') {
719 case 'main':
720 return this._dockPanel.widgets();
721 case 'left':
722 return iter(this._leftHandler.sideBar.titles.map(t => t.owner));
723 case 'right':
724 return iter(this._rightHandler.sideBar.titles.map(t => t.owner));
725 case 'header':
726 return this._headerPanel.children();
727 case 'top':
728 return this._topHandler.panel.children();
729 case 'menu':
730 return this._menuHandler.panel.children();
731 case 'bottom':
732 return this._bottomPanel.children();
733 default:
734 throw new Error(`Invalid area: ${area}`);
735 }
736 }
737 /**
738 * Handle `after-attach` messages for the application shell.
739 */
740 onAfterAttach(msg) {
741 this.node.dataset.shellMode = this.mode;
742 }
743 /**
744 * Update the title panel title based on the title of the current widget.
745 */
746 _updateTitlePanelTitle() {
747 let current = this.currentWidget;
748 const inputElement = this._titleHandler.inputElement;
749 inputElement.value = current ? current.title.label : '';
750 inputElement.title = current ? current.title.caption : '';
751 }
752 /**
753 * The path of the current widget changed, fire the _currentPathChanged signal.
754 */
755 _updateCurrentPath() {
756 let current = this.currentWidget;
757 let newValue = '';
758 if (current && current instanceof DocumentWidget) {
759 newValue = current.context.path;
760 }
761 this._currentPathChanged.emit({
762 newValue: newValue,
763 oldValue: this._currentPath
764 });
765 this._currentPath = newValue;
766 }
767 /**
768 * Add a widget to the left content area.
769 *
770 * #### Notes
771 * Widgets must have a unique `id` property, which will be used as the DOM id.
772 */
773 _addToLeftArea(widget, options) {
774 if (!widget.id) {
775 console.error('Widgets added to app shell must have unique id property.');
776 return;
777 }
778 options = options || this._sideOptionsCache.get(widget) || {};
779 this._sideOptionsCache.set(widget, options);
780 const rank = 'rank' in options ? options.rank : DEFAULT_RANK;
781 this._leftHandler.addWidget(widget, rank);
782 this._onLayoutModified();
783 }
784 /**
785 * Add a widget to the main content area.
786 *
787 * #### Notes
788 * Widgets must have a unique `id` property, which will be used as the DOM id.
789 * All widgets added to the main area should be disposed after removal
790 * (disposal before removal will remove the widget automatically).
791 *
792 * In the options, `ref` defaults to `null`, `mode` defaults to `'tab-after'`,
793 * and `activate` defaults to `true`.
794 */
795 _addToMainArea(widget, options) {
796 if (!widget.id) {
797 console.error('Widgets added to app shell must have unique id property.');
798 return;
799 }
800 options = options || {};
801 const dock = this._dockPanel;
802 const mode = options.mode || 'tab-after';
803 let ref = this.currentWidget;
804 if (options.ref) {
805 ref = find(dock.widgets(), value => value.id === options.ref) || null;
806 }
807 const { title } = widget;
808 // Add widget ID to tab so that we can get a handle on the tab's widget
809 // (for context menu support)
810 title.dataset = Object.assign(Object.assign({}, title.dataset), { id: widget.id });
811 if (title.icon instanceof LabIcon) {
812 // bind an appropriate style to the icon
813 title.icon = title.icon.bindprops({
814 stylesheet: 'mainAreaTab'
815 });
816 }
817 else if (typeof title.icon === 'string' || !title.icon) {
818 // add some classes to help with displaying css background imgs
819 title.iconClass = classes(title.iconClass, 'jp-Icon');
820 }
821 dock.addWidget(widget, { mode, ref });
822 // The dock panel doesn't account for placement information while
823 // in single document mode, so upon rehydrating any widgets that were
824 // added will not be in the correct place. Cache the placement information
825 // here so that we can later rehydrate correctly.
826 if (dock.mode === 'single-document') {
827 this._mainOptionsCache.set(widget, options);
828 }
829 if (options.activate !== false) {
830 dock.activateWidget(widget);
831 }
832 }
833 /**
834 * Add a widget to the right content area.
835 *
836 * #### Notes
837 * Widgets must have a unique `id` property, which will be used as the DOM id.
838 */
839 _addToRightArea(widget, options) {
840 if (!widget.id) {
841 console.error('Widgets added to app shell must have unique id property.');
842 return;
843 }
844 options = options || this._sideOptionsCache.get(widget) || {};
845 const rank = 'rank' in options ? options.rank : DEFAULT_RANK;
846 this._sideOptionsCache.set(widget, options);
847 this._rightHandler.addWidget(widget, rank);
848 this._onLayoutModified();
849 }
850 /**
851 * Add a widget to the top content area.
852 *
853 * #### Notes
854 * Widgets must have a unique `id` property, which will be used as the DOM id.
855 */
856 _addToTopArea(widget, options) {
857 var _a;
858 if (!widget.id) {
859 console.error('Widgets added to app shell must have unique id property.');
860 return;
861 }
862 options = options || {};
863 const rank = (_a = options.rank) !== null && _a !== void 0 ? _a : DEFAULT_RANK;
864 this._topHandler.addWidget(widget, rank);
865 this._onLayoutModified();
866 if (this._topHandler.panel.isHidden) {
867 this._topHandler.panel.show();
868 }
869 }
870 /**
871 * Add a widget to the title content area.
872 *
873 * #### Notes
874 * Widgets must have a unique `id` property, which will be used as the DOM id.
875 */
876 _addToMenuArea(widget, options) {
877 var _a;
878 if (!widget.id) {
879 console.error('Widgets added to app shell must have unique id property.');
880 return;
881 }
882 options = options || {};
883 const rank = (_a = options.rank) !== null && _a !== void 0 ? _a : DEFAULT_RANK;
884 this._menuHandler.addWidget(widget, rank);
885 this._onLayoutModified();
886 if (this._menuHandler.panel.isHidden) {
887 this._menuHandler.panel.show();
888 }
889 }
890 /**
891 * Add a widget to the header content area.
892 *
893 * #### Notes
894 * Widgets must have a unique `id` property, which will be used as the DOM id.
895 */
896 _addToHeaderArea(widget, options) {
897 if (!widget.id) {
898 console.error('Widgets added to app shell must have unique id property.');
899 return;
900 }
901 // Temporary: widgets are added to the panel in order of insertion.
902 this._headerPanel.addWidget(widget);
903 this._onLayoutModified();
904 if (this._headerPanel.isHidden) {
905 this._headerPanel.show();
906 }
907 }
908 /**
909 * Add a widget to the bottom content area.
910 *
911 * #### Notes
912 * Widgets must have a unique `id` property, which will be used as the DOM id.
913 */
914 _addToBottomArea(widget, options) {
915 if (!widget.id) {
916 console.error('Widgets added to app shell must have unique id property.');
917 return;
918 }
919 // Temporary: widgets are added to the panel in order of insertion.
920 this._bottomPanel.addWidget(widget);
921 this._onLayoutModified();
922 if (this._bottomPanel.isHidden) {
923 this._bottomPanel.show();
924 }
925 }
926 _addToDownArea(widget, options) {
927 if (!widget.id) {
928 console.error('Widgets added to app shell must have unique id property.');
929 return;
930 }
931 options = options || {};
932 const { title } = widget;
933 // Add widget ID to tab so that we can get a handle on the tab's widget
934 // (for context menu support)
935 title.dataset = Object.assign(Object.assign({}, title.dataset), { id: widget.id });
936 if (title.icon instanceof LabIcon) {
937 // bind an appropriate style to the icon
938 title.icon = title.icon.bindprops({
939 stylesheet: 'mainAreaTab'
940 });
941 }
942 else if (typeof title.icon === 'string' || !title.icon) {
943 // add some classes to help with displaying css background imgs
944 title.iconClass = classes(title.iconClass, 'jp-Icon');
945 }
946 this._downPanel.addWidget(widget);
947 this._onLayoutModified();
948 if (this._downPanel.isHidden) {
949 this._downPanel.show();
950 }
951 }
952 /*
953 * Return the tab bar adjacent to the current TabBar or `null`.
954 */
955 _adjacentBar(direction) {
956 const current = this._currentTabBar();
957 if (!current) {
958 return null;
959 }
960 const bars = toArray(this._dockPanel.tabBars());
961 const len = bars.length;
962 const index = bars.indexOf(current);
963 if (direction === 'previous') {
964 return index > 0 ? bars[index - 1] : index === 0 ? bars[len - 1] : null;
965 }
966 // Otherwise, direction is 'next'.
967 return index < len - 1
968 ? bars[index + 1]
969 : index === len - 1
970 ? bars[0]
971 : null;
972 }
973 /*
974 * Return the TabBar that has the currently active Widget or null.
975 */
976 _currentTabBar() {
977 const current = this._tracker.currentWidget;
978 if (!current) {
979 return null;
980 }
981 const title = current.title;
982 const bars = this._dockPanel.tabBars();
983 return find(bars, bar => bar.titles.indexOf(title) > -1) || null;
984 }
985 /**
986 * Handle a change to the dock area active widget.
987 */
988 _onActiveChanged(sender, args) {
989 if (args.newValue) {
990 args.newValue.title.className += ` ${ACTIVE_CLASS}`;
991 }
992 if (args.oldValue) {
993 args.oldValue.title.className = args.oldValue.title.className.replace(ACTIVE_CLASS, '');
994 }
995 this._activeChanged.emit(args);
996 }
997 /**
998 * Handle a change to the dock area current widget.
999 */
1000 _onCurrentChanged(sender, args) {
1001 if (args.newValue) {
1002 args.newValue.title.className += ` ${CURRENT_CLASS}`;
1003 }
1004 if (args.oldValue) {
1005 args.oldValue.title.className = args.oldValue.title.className.replace(CURRENT_CLASS, '');
1006 }
1007 this._currentChanged.emit(args);
1008 this._onLayoutModified();
1009 }
1010 /**
1011 * Handle a change on the down panel widgets
1012 */
1013 _onTabPanelChanged() {
1014 if (this._downPanel.stackedPanel.widgets.length === 0) {
1015 this._downPanel.hide();
1016 }
1017 this._onLayoutModified();
1018 }
1019 /**
1020 * Handle a change to the layout.
1021 */
1022 _onLayoutModified() {
1023 void this._layoutDebouncer.invoke();
1024 }
1025}
1026var Private;
1027(function (Private) {
1028 /**
1029 * A less-than comparison function for side bar rank items.
1030 */
1031 function itemCmp(first, second) {
1032 return first.rank - second.rank;
1033 }
1034 Private.itemCmp = itemCmp;
1035 /**
1036 * Removes widgets that have been disposed from an area config, mutates area.
1037 */
1038 function normalizeAreaConfig(parent, area) {
1039 if (!area) {
1040 return;
1041 }
1042 if (area.type === 'tab-area') {
1043 area.widgets = area.widgets.filter(widget => !widget.isDisposed && widget.parent === parent);
1044 return;
1045 }
1046 area.children.forEach(child => {
1047 normalizeAreaConfig(parent, child);
1048 });
1049 }
1050 Private.normalizeAreaConfig = normalizeAreaConfig;
1051 /**
1052 * A class which manages a panel and sorts its widgets by rank.
1053 */
1054 class PanelHandler {
1055 constructor() {
1056 /**
1057 * A message hook for child add/remove messages on the main area dock panel.
1058 */
1059 this._panelChildHook = (handler, msg) => {
1060 switch (msg.type) {
1061 case 'child-added':
1062 {
1063 const widget = msg.child;
1064 // If we already know about this widget, we're done
1065 if (this._items.find(v => v.widget === widget)) {
1066 break;
1067 }
1068 // Otherwise, add to the end by default
1069 const rank = this._items[this._items.length - 1].rank;
1070 this._items.push({ widget, rank });
1071 }
1072 break;
1073 case 'child-removed':
1074 {
1075 const widget = msg.child;
1076 ArrayExt.removeFirstWhere(this._items, v => v.widget === widget);
1077 }
1078 break;
1079 default:
1080 break;
1081 }
1082 return true;
1083 };
1084 this._items = new Array();
1085 this._panel = new Panel();
1086 MessageLoop.installMessageHook(this._panel, this._panelChildHook);
1087 }
1088 /**
1089 * Get the panel managed by the handler.
1090 */
1091 get panel() {
1092 return this._panel;
1093 }
1094 /**
1095 * Add a widget to the panel.
1096 *
1097 * If the widget is already added, it will be moved.
1098 */
1099 addWidget(widget, rank) {
1100 widget.parent = null;
1101 const item = { widget, rank };
1102 const index = ArrayExt.upperBound(this._items, item, Private.itemCmp);
1103 ArrayExt.insert(this._items, index, item);
1104 this._panel.insertWidget(index, widget);
1105 }
1106 }
1107 Private.PanelHandler = PanelHandler;
1108 /**
1109 * A class which manages a side bar and related stacked panel.
1110 */
1111 class SideBarHandler {
1112 /**
1113 * Construct a new side bar handler.
1114 */
1115 constructor() {
1116 this._items = new Array();
1117 this._sideBar = new TabBar({
1118 insertBehavior: 'none',
1119 removeBehavior: 'none',
1120 allowDeselect: true,
1121 orientation: 'vertical'
1122 });
1123 this._stackedPanel = new StackedPanel();
1124 this._sideBar.hide();
1125 this._stackedPanel.hide();
1126 this._lastCurrent = null;
1127 this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
1128 this._sideBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
1129 this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
1130 }
1131 /**
1132 * Get the tab bar managed by the handler.
1133 */
1134 get sideBar() {
1135 return this._sideBar;
1136 }
1137 /**
1138 * Get the stacked panel managed by the handler
1139 */
1140 get stackedPanel() {
1141 return this._stackedPanel;
1142 }
1143 /**
1144 * Expand the sidebar.
1145 *
1146 * #### Notes
1147 * This will open the most recently used tab, or the first tab
1148 * if there is no most recently used.
1149 */
1150 expand() {
1151 const previous = this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
1152 if (previous) {
1153 this.activate(previous.id);
1154 }
1155 }
1156 /**
1157 * Activate a widget residing in the side bar by ID.
1158 *
1159 * @param id - The widget's unique ID.
1160 */
1161 activate(id) {
1162 const widget = this._findWidgetByID(id);
1163 if (widget) {
1164 this._sideBar.currentTitle = widget.title;
1165 widget.activate();
1166 }
1167 }
1168 /**
1169 * Test whether the sidebar has the given widget by id.
1170 */
1171 has(id) {
1172 return this._findWidgetByID(id) !== null;
1173 }
1174 /**
1175 * Collapse the sidebar so no items are expanded.
1176 */
1177 collapse() {
1178 this._sideBar.currentTitle = null;
1179 }
1180 /**
1181 * Add a widget and its title to the stacked panel and side bar.
1182 *
1183 * If the widget is already added, it will be moved.
1184 */
1185 addWidget(widget, rank) {
1186 widget.parent = null;
1187 widget.hide();
1188 const item = { widget, rank };
1189 const index = this._findInsertIndex(item);
1190 ArrayExt.insert(this._items, index, item);
1191 this._stackedPanel.insertWidget(index, widget);
1192 const title = this._sideBar.insertTab(index, widget.title);
1193 // Store the parent id in the title dataset
1194 // in order to dispatch click events to the right widget.
1195 title.dataset = { id: widget.id };
1196 if (title.icon instanceof LabIcon) {
1197 // bind an appropriate style to the icon
1198 title.icon = title.icon.bindprops({
1199 stylesheet: 'sideBar'
1200 });
1201 }
1202 else if (typeof title.icon === 'string' || !title.icon) {
1203 // add some classes to help with displaying css background imgs
1204 title.iconClass = classes(title.iconClass, 'jp-Icon', 'jp-Icon-20');
1205 }
1206 this._refreshVisibility();
1207 }
1208 /**
1209 * Dehydrate the side bar data.
1210 */
1211 dehydrate() {
1212 const collapsed = this._sideBar.currentTitle === null;
1213 const widgets = toArray(this._stackedPanel.widgets);
1214 const currentWidget = widgets[this._sideBar.currentIndex];
1215 return { collapsed, currentWidget, widgets };
1216 }
1217 /**
1218 * Rehydrate the side bar.
1219 */
1220 rehydrate(data) {
1221 if (data.currentWidget) {
1222 this.activate(data.currentWidget.id);
1223 }
1224 if (data.collapsed) {
1225 this.collapse();
1226 }
1227 }
1228 /**
1229 * Find the insertion index for a rank item.
1230 */
1231 _findInsertIndex(item) {
1232 return ArrayExt.upperBound(this._items, item, Private.itemCmp);
1233 }
1234 /**
1235 * Find the index of the item with the given widget, or `-1`.
1236 */
1237 _findWidgetIndex(widget) {
1238 return ArrayExt.findFirstIndex(this._items, i => i.widget === widget);
1239 }
1240 /**
1241 * Find the widget which owns the given title, or `null`.
1242 */
1243 _findWidgetByTitle(title) {
1244 const item = find(this._items, value => value.widget.title === title);
1245 return item ? item.widget : null;
1246 }
1247 /**
1248 * Find the widget with the given id, or `null`.
1249 */
1250 _findWidgetByID(id) {
1251 const item = find(this._items, value => value.widget.id === id);
1252 return item ? item.widget : null;
1253 }
1254 /**
1255 * Refresh the visibility of the side bar and stacked panel.
1256 */
1257 _refreshVisibility() {
1258 this._sideBar.setHidden(this._sideBar.titles.length === 0);
1259 this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
1260 }
1261 /**
1262 * Handle the `currentChanged` signal from the sidebar.
1263 */
1264 _onCurrentChanged(sender, args) {
1265 const oldWidget = args.previousTitle
1266 ? this._findWidgetByTitle(args.previousTitle)
1267 : null;
1268 const newWidget = args.currentTitle
1269 ? this._findWidgetByTitle(args.currentTitle)
1270 : null;
1271 if (oldWidget) {
1272 oldWidget.hide();
1273 }
1274 if (newWidget) {
1275 newWidget.show();
1276 }
1277 this._lastCurrent = newWidget || oldWidget;
1278 this._refreshVisibility();
1279 }
1280 /**
1281 * Handle a `tabActivateRequest` signal from the sidebar.
1282 */
1283 _onTabActivateRequested(sender, args) {
1284 args.title.owner.activate();
1285 }
1286 /*
1287 * Handle the `widgetRemoved` signal from the stacked panel.
1288 */
1289 _onWidgetRemoved(sender, widget) {
1290 if (widget === this._lastCurrent) {
1291 this._lastCurrent = null;
1292 }
1293 ArrayExt.removeAt(this._items, this._findWidgetIndex(widget));
1294 this._sideBar.removeTab(widget.title);
1295 this._refreshVisibility();
1296 }
1297 }
1298 Private.SideBarHandler = SideBarHandler;
1299 class SkipLinkWidget extends Widget {
1300 /**
1301 * Construct a new skipLink widget.
1302 */
1303 constructor(shell) {
1304 super();
1305 this.addClass('jp-skiplink');
1306 this.id = 'jp-skiplink';
1307 this._shell = shell;
1308 this._createSkipLink('Skip to left side bar');
1309 }
1310 handleEvent(event) {
1311 switch (event.type) {
1312 case 'click':
1313 this._focusLeftSideBar();
1314 break;
1315 }
1316 }
1317 /**
1318 * Handle `after-attach` messages for the widget.
1319 */
1320 onAfterAttach(msg) {
1321 super.onAfterAttach(msg);
1322 this.node.addEventListener('click', this);
1323 }
1324 /**
1325 * A message handler invoked on a `'before-detach'`
1326 * message
1327 */
1328 onBeforeDetach(msg) {
1329 this.node.removeEventListener('click', this);
1330 super.onBeforeDetach(msg);
1331 }
1332 _focusLeftSideBar() {
1333 this._shell.expandLeft();
1334 }
1335 _createSkipLink(skipLinkText) {
1336 const skipLink = document.createElement('a');
1337 skipLink.href = '#';
1338 skipLink.tabIndex = 1;
1339 skipLink.text = skipLinkText;
1340 skipLink.className = 'skip-link';
1341 this.node.appendChild(skipLink);
1342 }
1343 }
1344 Private.SkipLinkWidget = SkipLinkWidget;
1345 class TitleHandler extends Widget {
1346 /**
1347 * Construct a new title handler.
1348 */
1349 constructor(shell) {
1350 super();
1351 this._selected = false;
1352 const inputElement = document.createElement('input');
1353 inputElement.type = 'text';
1354 this.node.appendChild(inputElement);
1355 this._shell = shell;
1356 this.id = 'jp-title-panel-title';
1357 }
1358 /**
1359 * Handle `after-attach` messages for the widget.
1360 */
1361 onAfterAttach(msg) {
1362 super.onAfterAttach(msg);
1363 this.inputElement.addEventListener('keyup', this);
1364 this.inputElement.addEventListener('click', this);
1365 this.inputElement.addEventListener('blur', this);
1366 }
1367 /**
1368 * Handle `before-detach` messages for the widget.
1369 */
1370 onBeforeDetach(msg) {
1371 super.onBeforeDetach(msg);
1372 this.inputElement.removeEventListener('keyup', this);
1373 this.inputElement.removeEventListener('click', this);
1374 this.inputElement.removeEventListener('blur', this);
1375 }
1376 handleEvent(event) {
1377 switch (event.type) {
1378 case 'keyup':
1379 void this._evtKeyUp(event);
1380 break;
1381 case 'click':
1382 this._evtClick(event);
1383 break;
1384 case 'blur':
1385 this._selected = false;
1386 break;
1387 }
1388 }
1389 /**
1390 * Handle `keyup` events on the handler.
1391 */
1392 async _evtKeyUp(event) {
1393 if (event.key == 'Enter') {
1394 const widget = this._shell.currentWidget;
1395 if (widget == null) {
1396 return;
1397 }
1398 const oldName = widget.title.label;
1399 const inputElement = this.inputElement;
1400 const newName = inputElement.value;
1401 inputElement.blur();
1402 if (newName !== oldName) {
1403 widget.title.label = newName;
1404 }
1405 else {
1406 inputElement.value = oldName;
1407 }
1408 }
1409 }
1410 /**
1411 * Handle `click` events on the handler.
1412 */
1413 _evtClick(event) {
1414 // only handle primary button clicks
1415 if (event.button !== 0 || this._selected) {
1416 return;
1417 }
1418 const inputElement = this.inputElement;
1419 event.preventDefault();
1420 event.stopPropagation();
1421 this._selected = true;
1422 const selectEnd = inputElement.value.indexOf('.');
1423 if (selectEnd === -1) {
1424 inputElement.select();
1425 }
1426 else {
1427 inputElement.setSelectionRange(0, selectEnd);
1428 }
1429 }
1430 /**
1431 * The input element containing the parent widget's title.
1432 */
1433 get inputElement() {
1434 return this.node.children[0];
1435 }
1436 }
1437 Private.TitleHandler = TitleHandler;
1438 class RestorableSplitPanel extends SplitPanel {
1439 constructor(options = {}) {
1440 super(options);
1441 this.updated = new Signal(this);
1442 }
1443 /**
1444 * Emit 'updated' signal on 'update' requests.
1445 */
1446 onUpdateRequest(msg) {
1447 super.onUpdateRequest(msg);
1448 this.updated.emit();
1449 }
1450 }
1451 Private.RestorableSplitPanel = RestorableSplitPanel;
1452})(Private || (Private = {}));
1453//# sourceMappingURL=shell.js.map
\No newline at end of file