shell.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. each, toArray
  5. } from 'phosphor/lib/algorithm/iteration';
  6. import {
  7. find, findIndex, upperBound
  8. } from 'phosphor/lib/algorithm/searching';
  9. import {
  10. Vector
  11. } from 'phosphor/lib/collections/vector';
  12. import {
  13. defineSignal, ISignal
  14. } from 'phosphor/lib/core/signaling';
  15. import {
  16. BoxLayout, BoxPanel
  17. } from 'phosphor/lib/ui/boxpanel';
  18. import {
  19. DockPanel
  20. } from 'phosphor/lib/ui/dockpanel';
  21. import {
  22. FocusTracker
  23. } from 'phosphor/lib/ui/focustracker';
  24. import {
  25. Panel
  26. } from 'phosphor/lib/ui/panel';
  27. import {
  28. SplitPanel
  29. } from 'phosphor/lib/ui/splitpanel';
  30. import {
  31. StackedPanel
  32. } from 'phosphor/lib/ui/stackedpanel';
  33. import {
  34. TabBar
  35. } from 'phosphor/lib/ui/tabbar';
  36. import {
  37. Title
  38. } from 'phosphor/lib/ui/title';
  39. import {
  40. Widget
  41. } from 'phosphor/lib/ui/widget';
  42. /**
  43. * The class name added to AppShell instances.
  44. */
  45. const APPLICATION_SHELL_CLASS = 'jp-ApplicationShell';
  46. /**
  47. * The class name added to side bar instances.
  48. */
  49. const SIDEBAR_CLASS = 'jp-SideBar';
  50. /**
  51. * The class name added to the current widget's title.
  52. */
  53. const CURRENT_CLASS = 'jp-mod-current';
  54. /**
  55. * The options for adding a widget to a side area of the shell.
  56. */
  57. export
  58. interface ISideAreaOptions {
  59. /**
  60. * The rank order of the widget among its siblings.
  61. */
  62. rank?: number;
  63. }
  64. /**
  65. * The application shell for JupyterLab.
  66. */
  67. export
  68. class ApplicationShell extends Widget {
  69. /**
  70. * Construct a new application shell.
  71. */
  72. constructor() {
  73. super();
  74. this.addClass(APPLICATION_SHELL_CLASS);
  75. this.id = 'main';
  76. let topPanel = this._topPanel = new Panel();
  77. let hboxPanel = this._hboxPanel = new BoxPanel();
  78. let dockPanel = this._dockPanel = new DockPanel();
  79. let hsplitPanel = this._hsplitPanel = new SplitPanel();
  80. let leftHandler = this._leftHandler = new SideBarHandler('left');
  81. let rightHandler = this._rightHandler = new SideBarHandler('right');
  82. let rootLayout = new BoxLayout();
  83. topPanel.id = 'jp-top-panel';
  84. hboxPanel.id = 'jp-main-content-panel';
  85. dockPanel.id = 'jp-main-dock-panel';
  86. hsplitPanel.id = 'jp-main-split-panel';
  87. leftHandler.sideBar.addClass(SIDEBAR_CLASS);
  88. leftHandler.sideBar.addClass('jp-mod-left');
  89. leftHandler.stackedPanel.id = 'jp-left-stack';
  90. rightHandler.sideBar.addClass(SIDEBAR_CLASS);
  91. rightHandler.sideBar.addClass('jp-mod-right');
  92. rightHandler.stackedPanel.id = 'jp-right-stack';
  93. hboxPanel.spacing = 0;
  94. dockPanel.spacing = 5;
  95. hsplitPanel.spacing = 1;
  96. hboxPanel.direction = 'left-to-right';
  97. hsplitPanel.orientation = 'horizontal';
  98. SplitPanel.setStretch(leftHandler.stackedPanel, 0);
  99. SplitPanel.setStretch(dockPanel, 1);
  100. SplitPanel.setStretch(rightHandler.stackedPanel, 0);
  101. BoxPanel.setStretch(leftHandler.sideBar, 0);
  102. BoxPanel.setStretch(hsplitPanel, 1);
  103. BoxPanel.setStretch(rightHandler.sideBar, 0);
  104. hsplitPanel.addWidget(leftHandler.stackedPanel);
  105. hsplitPanel.addWidget(dockPanel);
  106. hsplitPanel.addWidget(rightHandler.stackedPanel);
  107. hboxPanel.addWidget(leftHandler.sideBar);
  108. hboxPanel.addWidget(hsplitPanel);
  109. hboxPanel.addWidget(rightHandler.sideBar);
  110. rootLayout.direction = 'top-to-bottom';
  111. rootLayout.spacing = 0; // TODO make this configurable?
  112. BoxLayout.setStretch(topPanel, 0);
  113. BoxLayout.setStretch(hboxPanel, 1);
  114. rootLayout.addWidget(topPanel);
  115. rootLayout.addWidget(hboxPanel);
  116. this.layout = rootLayout;
  117. this._dockPanel.currentChanged.connect((sender, args) => {
  118. if (args.newValue) {
  119. args.newValue.title.className += ` ${CURRENT_CLASS}`;
  120. }
  121. if (args.oldValue) {
  122. let title = args.oldValue.title;
  123. title.className = title.className.replace(CURRENT_CLASS, '');
  124. }
  125. this.currentChanged.emit(args);
  126. });
  127. }
  128. /**
  129. * A signal emitted when main area's current focus changes.
  130. */
  131. readonly currentChanged: ISignal<this, FocusTracker.ICurrentChangedArgs<Widget>>;
  132. /**
  133. * The current widget in the shell's main area.
  134. *
  135. * #### Notes
  136. * This property is read-only.
  137. */
  138. get currentWidget(): Widget {
  139. return this._dockPanel.currentWidget;
  140. }
  141. /**
  142. * Add a widget to the top content area.
  143. *
  144. * #### Notes
  145. * Widgets must have a unique `id` property, which will be used as the DOM id.
  146. */
  147. addToTopArea(widget: Widget, options: ISideAreaOptions = {}): void {
  148. if (!widget.id) {
  149. console.error('widgets added to app shell must have unique id property');
  150. return;
  151. }
  152. // Temporary: widgets are added to the panel in order of insertion.
  153. this._topPanel.addWidget(widget);
  154. }
  155. /**
  156. * Add a widget to the left content area.
  157. *
  158. * #### Notes
  159. * Widgets must have a unique `id` property, which will be used as the DOM id.
  160. */
  161. addToLeftArea(widget: Widget, options: ISideAreaOptions = {}): void {
  162. if (!widget.id) {
  163. console.error('widgets added to app shell must have unique id property');
  164. return;
  165. }
  166. let rank = 'rank' in options ? options.rank : 100;
  167. this._leftHandler.addWidget(widget, rank);
  168. }
  169. /**
  170. * Add a widget to the right content area.
  171. *
  172. * #### Notes
  173. * Widgets must have a unique `id` property, which will be used as the DOM id.
  174. */
  175. addToRightArea(widget: Widget, options: ISideAreaOptions = {}): void {
  176. if (!widget.id) {
  177. console.error('widgets added to app shell must have unique id property');
  178. return;
  179. }
  180. let rank = 'rank' in options ? options.rank : 100;
  181. this._rightHandler.addWidget(widget, rank);
  182. }
  183. /**
  184. * Add a widget to the main content area.
  185. *
  186. * #### Notes
  187. * Widgets must have a unique `id` property, which will be used as the DOM id.
  188. */
  189. addToMainArea(widget: Widget): void {
  190. // TODO
  191. if (!widget.id) {
  192. console.error('widgets added to app shell must have unique id property');
  193. return;
  194. }
  195. this._dockPanel.addWidget(widget, { mode: 'tab-after' });
  196. }
  197. /**
  198. * Activate a widget in the left area.
  199. */
  200. activateLeft(id: string): void {
  201. this._leftHandler.activate(id);
  202. }
  203. /**
  204. * Activate a widget in the right area.
  205. */
  206. activateRight(id: string): void {
  207. this._rightHandler.activate(id);
  208. }
  209. /**
  210. * Activate a widget in the main area.
  211. */
  212. activateMain(id: string): void {
  213. let dock = this._dockPanel;
  214. let widget = find(dock.widgets(), value => value.id === id);
  215. if (widget) {
  216. dock.activateWidget(widget);
  217. }
  218. }
  219. /**
  220. * Collapse the left area.
  221. */
  222. collapseLeft(): void {
  223. this._leftHandler.collapse();
  224. }
  225. /**
  226. * Collapse the right area.
  227. */
  228. collapseRight(): void {
  229. this._rightHandler.collapse();
  230. }
  231. /**
  232. * Close all tracked widgets.
  233. */
  234. closeAll(): void {
  235. each(toArray(this._dockPanel.widgets()), widget => { widget.close(); });
  236. }
  237. private _topPanel: Panel;
  238. private _hboxPanel: BoxPanel;
  239. private _dockPanel: DockPanel;
  240. private _hsplitPanel: SplitPanel;
  241. private _leftHandler: SideBarHandler;
  242. private _rightHandler: SideBarHandler;
  243. }
  244. // Define the signals for the `ApplicationShell` class.
  245. defineSignal(ApplicationShell.prototype, 'currentChanged');
  246. /**
  247. * A class which manages a side bar and related stacked panel.
  248. */
  249. class SideBarHandler {
  250. /**
  251. * Construct a new side bar handler.
  252. */
  253. constructor(side: string) {
  254. this._side = side;
  255. this._sideBar = new TabBar({
  256. insertBehavior: 'none',
  257. removeBehavior: 'none',
  258. allowDeselect: true
  259. });
  260. this._stackedPanel = new StackedPanel();
  261. this._sideBar.hide();
  262. this._stackedPanel.hide();
  263. this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
  264. this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
  265. }
  266. /**
  267. * Get the tab bar managed by the handler.
  268. */
  269. get sideBar(): TabBar {
  270. return this._sideBar;
  271. }
  272. /**
  273. * Get the stacked panel managed by the handler
  274. */
  275. get stackedPanel(): StackedPanel {
  276. return this._stackedPanel;
  277. }
  278. /**
  279. * Activate a widget residing in the side bar by ID.
  280. *
  281. * @param id - The widget's unique ID.
  282. */
  283. activate(id: string): void {
  284. let widget = this._findWidgetByID(id);
  285. if (widget) {
  286. this._sideBar.currentTitle = widget.title;
  287. widget.activate();
  288. }
  289. }
  290. /**
  291. * Collapse the sidebar so no items are expanded.
  292. */
  293. collapse(): void {
  294. this._sideBar.currentTitle = null;
  295. }
  296. /**
  297. * Add a widget and its title to the stacked panel and side bar.
  298. *
  299. * If the widget is already added, it will be moved.
  300. */
  301. addWidget(widget: Widget, rank: number): void {
  302. widget.parent = null;
  303. widget.hide();
  304. let item = { widget, rank };
  305. let index = this._findInsertIndex(item);
  306. this._items.insert(index, item);
  307. this._stackedPanel.insertWidget(index, widget);
  308. this._sideBar.insertTab(index, widget.title);
  309. this._refreshVisibility();
  310. }
  311. /**
  312. * Find the insertion index for a rank item.
  313. */
  314. private _findInsertIndex(item: Private.IRankItem): number {
  315. return upperBound(this._items, item, Private.itemCmp);
  316. }
  317. /**
  318. * Find the index of the item with the given widget, or `-1`.
  319. */
  320. private _findWidgetIndex(widget: Widget): number {
  321. return findIndex(this._items, item => item.widget === widget);
  322. }
  323. /**
  324. * Find the widget which owns the given title, or `null`.
  325. */
  326. private _findWidgetByTitle(title: Title): Widget {
  327. let item = find(this._items, value => value.widget.title === title);
  328. return item ? item.widget : null;
  329. }
  330. /**
  331. * Find the widget with the given id, or `null`.
  332. */
  333. private _findWidgetByID(id: string): Widget {
  334. let item = find(this._items, value => value.widget.id === id);
  335. return item ? item.widget : null;
  336. }
  337. /**
  338. * Refresh the visibility of the side bar and stacked panel.
  339. */
  340. private _refreshVisibility(): void {
  341. this._sideBar.setHidden(this._sideBar.titles.length === 0);
  342. this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
  343. }
  344. /**
  345. * Handle the `currentChanged` signal from the sidebar.
  346. */
  347. private _onCurrentChanged(sender: TabBar, args: TabBar.ICurrentChangedArgs): void {
  348. let oldWidget = this._findWidgetByTitle(args.previousTitle);
  349. let newWidget = this._findWidgetByTitle(args.currentTitle);
  350. if (oldWidget) {
  351. oldWidget.hide();
  352. }
  353. if (newWidget) {
  354. newWidget.show();
  355. }
  356. if (newWidget) {
  357. document.body.setAttribute(`data-${this._side}Area`, newWidget.id);
  358. } else {
  359. document.body.removeAttribute(`data-${this._side}Area`);
  360. }
  361. this._refreshVisibility();
  362. }
  363. /*
  364. * Handle the `widgetRemoved` signal from the stacked panel.
  365. */
  366. private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
  367. this._items.removeAt(this._findWidgetIndex(widget));
  368. this._sideBar.removeTab(widget.title);
  369. this._refreshVisibility();
  370. }
  371. private _side: string;
  372. private _sideBar: TabBar;
  373. private _stackedPanel: StackedPanel;
  374. private _items = new Vector<Private.IRankItem>();
  375. }
  376. namespace Private {
  377. export
  378. /**
  379. * An object which holds a widget and its sort rank.
  380. */
  381. interface IRankItem {
  382. /**
  383. * The widget for the item.
  384. */
  385. widget: Widget;
  386. /**
  387. * The sort rank of the widget.
  388. */
  389. rank: number;
  390. }
  391. /**
  392. * A less-than comparison function for side bar rank items.
  393. */
  394. export
  395. function itemCmp(first: IRankItem, second: IRankItem): number {
  396. return first.rank - second.rank;
  397. }
  398. }