shell.ts 41 KB

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