@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/lib/frontend.d.ts +2 -3
- package/lib/frontend.js.map +1 -1
- package/lib/lab.js +7 -3
- package/lib/lab.js.map +1 -1
- package/lib/layoutrestorer.d.ts +23 -2
- package/lib/layoutrestorer.js +71 -22
- package/lib/layoutrestorer.js.map +1 -1
- package/lib/mimerenderers.js +1 -1
- package/lib/mimerenderers.js.map +1 -1
- package/lib/shell.d.ts +110 -19
- package/lib/shell.js +197 -66
- package/lib/shell.js.map +1 -1
- package/lib/tokens.d.ts +1 -1
- package/lib/utils.js +9 -3
- package/lib/utils.js.map +1 -1
- package/package.json +27 -27
- package/src/connectionlost.ts +27 -0
- package/src/frontend.ts +412 -0
- package/src/index.ts +25 -0
- package/src/lab.ts +289 -0
- package/src/layoutrestorer.ts +870 -0
- package/src/mimerenderers.ts +180 -0
- package/src/router.ts +206 -0
- package/src/shell.ts +2285 -0
- package/src/status.ts +93 -0
- package/src/tokens.ts +220 -0
- package/src/treepathupdater.ts +20 -0
- package/src/utils.ts +153 -0
- package/style/buttons.css +10 -4
- package/style/core.css +4 -0
- package/style/menus.css +22 -3
- package/style/scrollbar.css +1 -1
- package/style/sidepanel.css +58 -81
- package/style/tabs.css +16 -23
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
|
+
}
|