shell.ts 11 KB

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