index.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IConnectionLost,
  5. ILabShell,
  6. ILabStatus,
  7. ILayoutRestorer,
  8. IRouter,
  9. ConnectionLost,
  10. JupyterFrontEnd,
  11. JupyterFrontEndPlugin,
  12. JupyterLab,
  13. LabShell,
  14. LayoutRestorer,
  15. Router
  16. } from '@jupyterlab/application';
  17. import {
  18. Dialog,
  19. ICommandPalette,
  20. IWindowResolver,
  21. showDialog,
  22. showErrorMessage
  23. } from '@jupyterlab/apputils';
  24. import { PathExt, URLExt } from '@jupyterlab/coreutils';
  25. import {
  26. IPropertyInspectorProvider,
  27. SideBarPropertyInspectorProvider
  28. } from '@jupyterlab/property-inspector';
  29. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  30. import { IStateDB } from '@jupyterlab/statedb';
  31. import { buildIcon } from '@jupyterlab/ui-components';
  32. import { each, iter, toArray } from '@lumino/algorithm';
  33. import { PromiseDelegate } from '@lumino/coreutils';
  34. import { DisposableDelegate, DisposableSet } from '@lumino/disposable';
  35. import { Widget, DockLayout } from '@lumino/widgets';
  36. import * as React from 'react';
  37. /**
  38. * The command IDs used by the application plugin.
  39. */
  40. namespace CommandIDs {
  41. export const activateNextTab: string = 'application:activate-next-tab';
  42. export const activatePreviousTab: string =
  43. 'application:activate-previous-tab';
  44. export const activateNextTabBar: string = 'application:activate-next-tab-bar';
  45. export const activatePreviousTabBar: string =
  46. 'application:activate-previous-tab-bar';
  47. export const close = 'application:close';
  48. export const closeOtherTabs = 'application:close-other-tabs';
  49. export const closeRightTabs = 'application:close-right-tabs';
  50. export const closeAll: string = 'application:close-all';
  51. export const setMode: string = 'application:set-mode';
  52. export const toggleMode: string = 'application:toggle-mode';
  53. export const toggleLeftArea: string = 'application:toggle-left-area';
  54. export const toggleRightArea: string = 'application:toggle-right-area';
  55. export const togglePresentationMode: string =
  56. 'application:toggle-presentation-mode';
  57. export const tree: string = 'router:tree';
  58. export const switchSidebar = 'sidebar:switch';
  59. }
  60. /**
  61. * The main extension.
  62. */
  63. const main: JupyterFrontEndPlugin<void> = {
  64. id: '@jupyterlab/application-extension:main',
  65. requires: [IRouter, IWindowResolver],
  66. optional: [ICommandPalette, IConnectionLost],
  67. activate: (
  68. app: JupyterFrontEnd,
  69. router: IRouter,
  70. resolver: IWindowResolver,
  71. palette: ICommandPalette | null,
  72. connectionLost: IConnectionLost | null
  73. ) => {
  74. if (!(app instanceof JupyterLab)) {
  75. throw new Error(`${main.id} must be activated in JupyterLab.`);
  76. }
  77. // Requiring the window resolver guarantees that the application extension
  78. // only loads if there is a viable window name. Otherwise, the application
  79. // will short-circuit and ask the user to navigate away.
  80. const workspace = resolver.name;
  81. console.log(`Starting application in workspace: "${workspace}"`);
  82. // If there were errors registering plugins, tell the user.
  83. if (app.registerPluginErrors.length !== 0) {
  84. const body = (
  85. <pre>{app.registerPluginErrors.map(e => e.message).join('\n')}</pre>
  86. );
  87. void showErrorMessage('Error Registering Plugins', { message: body });
  88. }
  89. addCommands(app, palette);
  90. // If the application shell layout is modified,
  91. // trigger a refresh of the commands.
  92. app.shell.layoutModified.connect(() => {
  93. app.commands.notifyCommandChanged();
  94. });
  95. // If the connection to the server is lost, handle it with the
  96. // connection lost handler.
  97. connectionLost = connectionLost || ConnectionLost;
  98. app.serviceManager.connectionFailure.connect(connectionLost);
  99. const builder = app.serviceManager.builder;
  100. const build = () => {
  101. return builder
  102. .build()
  103. .then(() => {
  104. return showDialog({
  105. title: 'Build Complete',
  106. body: 'Build successfully completed, reload page?',
  107. buttons: [
  108. Dialog.cancelButton(),
  109. Dialog.warnButton({ label: 'Reload' })
  110. ]
  111. });
  112. })
  113. .then(result => {
  114. if (result.button.accept) {
  115. router.reload();
  116. }
  117. })
  118. .catch(err => {
  119. void showErrorMessage('Build Failed', {
  120. message: <pre>{err.message}</pre>
  121. });
  122. });
  123. };
  124. if (builder.isAvailable && builder.shouldCheck) {
  125. void builder.getStatus().then(response => {
  126. if (response.status === 'building') {
  127. return build();
  128. }
  129. if (response.status !== 'needed') {
  130. return;
  131. }
  132. const body = (
  133. <div>
  134. JupyterLab build is suggested:
  135. <br />
  136. <pre>{response.message}</pre>
  137. </div>
  138. );
  139. void showDialog({
  140. title: 'Build Recommended',
  141. body,
  142. buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Build' })]
  143. }).then(result => (result.button.accept ? build() : undefined));
  144. });
  145. }
  146. const message =
  147. 'Are you sure you want to exit JupyterLab?\n' +
  148. 'Any unsaved changes will be lost.';
  149. // The spec for the `beforeunload` event is implemented differently by
  150. // the different browser vendors. Consequently, the `event.returnValue`
  151. // attribute needs to set in addition to a return value being returned.
  152. // For more information, see:
  153. // https://developer.mozilla.org/en/docs/Web/Events/beforeunload
  154. window.addEventListener('beforeunload', event => {
  155. if (app.status.isDirty) {
  156. return ((event as any).returnValue = message);
  157. }
  158. });
  159. },
  160. autoStart: true
  161. };
  162. /**
  163. * The default layout restorer provider.
  164. */
  165. const layout: JupyterFrontEndPlugin<ILayoutRestorer> = {
  166. id: '@jupyterlab/application-extension:layout',
  167. requires: [IStateDB, ILabShell],
  168. activate: (app: JupyterFrontEnd, state: IStateDB, labShell: ILabShell) => {
  169. const first = app.started;
  170. const registry = app.commands;
  171. const restorer = new LayoutRestorer({ connector: state, first, registry });
  172. void restorer.fetch().then(saved => {
  173. labShell.restoreLayout(saved);
  174. labShell.layoutModified.connect(() => {
  175. void restorer.save(labShell.saveLayout());
  176. });
  177. });
  178. return restorer;
  179. },
  180. autoStart: true,
  181. provides: ILayoutRestorer
  182. };
  183. /**
  184. * The default URL router provider.
  185. */
  186. const router: JupyterFrontEndPlugin<IRouter> = {
  187. id: '@jupyterlab/application-extension:router',
  188. requires: [JupyterFrontEnd.IPaths],
  189. activate: (app: JupyterFrontEnd, paths: JupyterFrontEnd.IPaths) => {
  190. const { commands } = app;
  191. const base = paths.urls.base;
  192. const router = new Router({ base, commands });
  193. void app.started.then(() => {
  194. // Route the very first request on load.
  195. void router.route();
  196. // Route all pop state events.
  197. window.addEventListener('popstate', () => {
  198. void router.route();
  199. });
  200. });
  201. return router;
  202. },
  203. autoStart: true,
  204. provides: IRouter
  205. };
  206. /**
  207. * The default tree route resolver plugin.
  208. */
  209. const tree: JupyterFrontEndPlugin<JupyterFrontEnd.ITreeResolver> = {
  210. id: '@jupyterlab/application-extension:tree-resolver',
  211. autoStart: true,
  212. requires: [JupyterFrontEnd.IPaths, IRouter, IWindowResolver],
  213. provides: JupyterFrontEnd.ITreeResolver,
  214. activate: (
  215. app: JupyterFrontEnd,
  216. paths: JupyterFrontEnd.IPaths,
  217. router: IRouter,
  218. resolver: IWindowResolver
  219. ): JupyterFrontEnd.ITreeResolver => {
  220. const { commands } = app;
  221. const treePattern = new RegExp(`^${paths.urls.tree}([^?]+)`);
  222. const workspacePattern = new RegExp(
  223. `^${paths.urls.workspaces}/[^?/]+/tree/([^?]+)`
  224. );
  225. const set = new DisposableSet();
  226. const delegate = new PromiseDelegate<JupyterFrontEnd.ITreeResolver.Paths>();
  227. set.add(
  228. commands.addCommand(CommandIDs.tree, {
  229. execute: async (args: IRouter.ILocation) => {
  230. if (set.isDisposed) {
  231. return;
  232. }
  233. const treeMatch = args.path.match(treePattern);
  234. const workspaceMatch = args.path.match(workspacePattern);
  235. const match = treeMatch || workspaceMatch;
  236. const file = match ? decodeURI(match[1]) : '';
  237. const workspace = PathExt.basename(resolver.name);
  238. const query = URLExt.queryStringToObject(args.search ?? '');
  239. const browser = query['file-browser-path'] || '';
  240. // Remove the file browser path from the query string.
  241. delete query['file-browser-path'];
  242. // Remove the tree portion of the URL.
  243. const url =
  244. (workspaceMatch
  245. ? URLExt.join(paths.urls.workspaces, workspace)
  246. : paths.urls.app) +
  247. URLExt.objectToQueryString(query) +
  248. args.hash;
  249. // Route to the cleaned URL.
  250. router.navigate(url);
  251. // Clean up artifacts immediately upon routing.
  252. set.dispose();
  253. delegate.resolve({ browser, file });
  254. }
  255. })
  256. );
  257. set.add(
  258. router.register({ command: CommandIDs.tree, pattern: treePattern })
  259. );
  260. set.add(
  261. router.register({ command: CommandIDs.tree, pattern: workspacePattern })
  262. );
  263. // If a route is handled by the router without the tree command being
  264. // invoked, resolve to `null` and clean up artifacts.
  265. const listener = () => {
  266. if (set.isDisposed) {
  267. return;
  268. }
  269. set.dispose();
  270. delegate.resolve(null);
  271. };
  272. router.routed.connect(listener);
  273. set.add(
  274. new DisposableDelegate(() => {
  275. router.routed.disconnect(listener);
  276. })
  277. );
  278. return { paths: delegate.promise };
  279. }
  280. };
  281. /**
  282. * The default URL not found extension.
  283. */
  284. const notfound: JupyterFrontEndPlugin<void> = {
  285. id: '@jupyterlab/application-extension:notfound',
  286. requires: [JupyterFrontEnd.IPaths, IRouter],
  287. activate: (
  288. _: JupyterFrontEnd,
  289. paths: JupyterFrontEnd.IPaths,
  290. router: IRouter
  291. ) => {
  292. const bad = paths.urls.notFound;
  293. if (!bad) {
  294. return;
  295. }
  296. const base = router.base;
  297. const message = `
  298. The path: ${bad} was not found. JupyterLab redirected to: ${base}
  299. `;
  300. // Change the URL back to the base application URL.
  301. router.navigate('');
  302. void showErrorMessage('Path Not Found', { message });
  303. },
  304. autoStart: true
  305. };
  306. /**
  307. * Change the favicon changing based on the busy status;
  308. */
  309. const busy: JupyterFrontEndPlugin<void> = {
  310. id: '@jupyterlab/application-extension:faviconbusy',
  311. requires: [ILabStatus],
  312. activate: async (_: JupyterFrontEnd, status: ILabStatus) => {
  313. status.busySignal.connect((_, isBusy) => {
  314. const favicon = document.querySelector(
  315. `link[rel="icon"]${isBusy ? '.idle.favicon' : '.busy.favicon'}`
  316. ) as HTMLLinkElement;
  317. if (!favicon) {
  318. return;
  319. }
  320. const newFavicon = document.querySelector(
  321. `link${isBusy ? '.busy.favicon' : '.idle.favicon'}`
  322. ) as HTMLLinkElement;
  323. if (!newFavicon) {
  324. return;
  325. }
  326. // If we have the two icons with the special classes, then toggle them.
  327. if (favicon !== newFavicon) {
  328. favicon.rel = '';
  329. newFavicon.rel = 'icon';
  330. // Firefox doesn't seem to recognize just changing rel, so we also
  331. // reinsert the link into the DOM.
  332. newFavicon.parentNode!.replaceChild(newFavicon, newFavicon);
  333. }
  334. });
  335. },
  336. autoStart: true
  337. };
  338. const SIDEBAR_ID = '@jupyterlab/application-extension:sidebar';
  339. /**
  340. * Keep user settings for where to show the side panels.
  341. */
  342. const sidebar: JupyterFrontEndPlugin<void> = {
  343. id: SIDEBAR_ID,
  344. activate: (
  345. app: JupyterFrontEnd,
  346. settingRegistry: ISettingRegistry,
  347. labShell: ILabShell
  348. ) => {
  349. type overrideMap = { [id: string]: 'left' | 'right' };
  350. let overrides: overrideMap = {};
  351. const handleLayoutOverrides = () => {
  352. each(labShell.widgets('left'), widget => {
  353. if (overrides[widget.id] && overrides[widget.id] === 'right') {
  354. labShell.add(widget, 'right');
  355. }
  356. });
  357. each(labShell.widgets('right'), widget => {
  358. if (overrides[widget.id] && overrides[widget.id] === 'left') {
  359. labShell.add(widget, 'left');
  360. }
  361. });
  362. };
  363. labShell.layoutModified.connect(handleLayoutOverrides);
  364. // Fetch overrides from the settings system.
  365. void Promise.all([settingRegistry.load(SIDEBAR_ID), app.restored]).then(
  366. ([settings]) => {
  367. overrides = (settings.get('overrides').composite as overrideMap) || {};
  368. settings.changed.connect(settings => {
  369. overrides =
  370. (settings.get('overrides').composite as overrideMap) || {};
  371. handleLayoutOverrides();
  372. });
  373. }
  374. );
  375. // Add a command to switch a side panels's side
  376. app.commands.addCommand(CommandIDs.switchSidebar, {
  377. label: 'Switch Sidebar Side',
  378. execute: () => {
  379. // First, try to find the correct panel based on the
  380. // application context menu click.
  381. const contextNode: HTMLElement | undefined = app.contextMenuHitTest(
  382. node => !!node.dataset.id
  383. );
  384. let id: string;
  385. let side: 'left' | 'right';
  386. if (contextNode) {
  387. id = contextNode.dataset['id']!;
  388. const leftPanel = document.getElementById('jp-left-stack');
  389. const node = document.getElementById(id);
  390. if (leftPanel && node && leftPanel.contains(node)) {
  391. side = 'right';
  392. } else {
  393. side = 'left';
  394. }
  395. } else {
  396. // Bail if we don't find a sidebar for the widget.
  397. return;
  398. }
  399. // Move the panel to the other side.
  400. const newOverrides = { ...overrides };
  401. newOverrides[id] = side;
  402. return settingRegistry.set(SIDEBAR_ID, 'overrides', newOverrides);
  403. }
  404. });
  405. // Add a context menu item to sidebar tabs.
  406. app.contextMenu.addItem({
  407. command: CommandIDs.switchSidebar,
  408. selector: '.jp-SideBar .lm-TabBar-tab',
  409. rank: 500
  410. });
  411. },
  412. requires: [ISettingRegistry, ILabShell],
  413. autoStart: true
  414. };
  415. /**
  416. * Add the main application commands.
  417. */
  418. function addCommands(app: JupyterLab, palette: ICommandPalette | null): void {
  419. const { commands, contextMenu, shell } = app;
  420. const category = 'Main Area';
  421. // Returns the widget associated with the most recent contextmenu event.
  422. const contextMenuWidget = (): Widget | null => {
  423. const test = (node: HTMLElement) => !!node.dataset.id;
  424. const node = app.contextMenuHitTest(test);
  425. if (!node) {
  426. // Fall back to active widget if path cannot be obtained from event.
  427. return shell.currentWidget;
  428. }
  429. const matches = toArray(shell.widgets('main')).filter(
  430. widget => widget.id === node.dataset.id
  431. );
  432. if (matches.length < 1) {
  433. return shell.currentWidget;
  434. }
  435. return matches[0];
  436. };
  437. // Closes an array of widgets.
  438. const closeWidgets = (widgets: Array<Widget>): void => {
  439. widgets.forEach(widget => widget.close());
  440. };
  441. // Find the tab area for a widget within a specific dock area.
  442. const findTab = (
  443. area: DockLayout.AreaConfig,
  444. widget: Widget
  445. ): DockLayout.ITabAreaConfig | null => {
  446. switch (area.type) {
  447. case 'split-area':
  448. const iterator = iter(area.children);
  449. let tab: DockLayout.ITabAreaConfig | null = null;
  450. let value: DockLayout.AreaConfig | undefined;
  451. do {
  452. value = iterator.next();
  453. if (value) {
  454. tab = findTab(value, widget);
  455. }
  456. } while (!tab && value);
  457. return tab;
  458. case 'tab-area':
  459. const { id } = widget;
  460. return area.widgets.some(widget => widget.id === id) ? area : null;
  461. default:
  462. return null;
  463. }
  464. };
  465. // Find the tab area for a widget within the main dock area.
  466. const tabAreaFor = (widget: Widget): DockLayout.ITabAreaConfig | null => {
  467. const { mainArea } = shell.saveLayout();
  468. if (!mainArea || mainArea.mode !== 'multiple-document') {
  469. return null;
  470. }
  471. let area = mainArea.dock?.main;
  472. if (!area) {
  473. return null;
  474. }
  475. return findTab(area, widget);
  476. };
  477. // Returns an array of all widgets to the right of a widget in a tab area.
  478. const widgetsRightOf = (widget: Widget): Array<Widget> => {
  479. const { id } = widget;
  480. const tabArea = tabAreaFor(widget);
  481. const widgets = tabArea ? tabArea.widgets || [] : [];
  482. const index = widgets.findIndex(widget => widget.id === id);
  483. if (index < 0) {
  484. return [];
  485. }
  486. return widgets.slice(index + 1);
  487. };
  488. commands.addCommand(CommandIDs.activateNextTab, {
  489. label: 'Activate Next Tab',
  490. execute: () => {
  491. shell.activateNextTab();
  492. }
  493. });
  494. commands.addCommand(CommandIDs.activatePreviousTab, {
  495. label: 'Activate Previous Tab',
  496. execute: () => {
  497. shell.activatePreviousTab();
  498. }
  499. });
  500. commands.addCommand(CommandIDs.activateNextTabBar, {
  501. label: 'Activate Next Tab Bar',
  502. execute: () => {
  503. shell.activateNextTabBar();
  504. }
  505. });
  506. commands.addCommand(CommandIDs.activatePreviousTabBar, {
  507. label: 'Activate Previous Tab Bar',
  508. execute: () => {
  509. shell.activatePreviousTabBar();
  510. }
  511. });
  512. // A CSS selector targeting tabs in the main area. This is a very
  513. // specific selector since we really only want tabs that are
  514. // in the main area, as opposed to those in sidebars, ipywidgets, etc.
  515. const tabSelector =
  516. '#jp-main-dock-panel .lm-DockPanel-tabBar.jp-Activity .lm-TabBar-tab';
  517. commands.addCommand(CommandIDs.close, {
  518. label: () => 'Close Tab',
  519. isEnabled: () =>
  520. !!shell.currentWidget && shell.currentWidget.title.closable,
  521. execute: () => {
  522. if (shell.currentWidget) {
  523. shell.currentWidget.close();
  524. }
  525. }
  526. });
  527. contextMenu.addItem({
  528. command: CommandIDs.close,
  529. selector: tabSelector,
  530. rank: 4
  531. });
  532. commands.addCommand(CommandIDs.closeAll, {
  533. label: 'Close All Tabs',
  534. execute: () => {
  535. shell.closeAll();
  536. }
  537. });
  538. commands.addCommand(CommandIDs.closeOtherTabs, {
  539. label: () => `Close All Other Tabs`,
  540. isEnabled: () => {
  541. // Ensure there are at least two widgets.
  542. const iterator = shell.widgets('main');
  543. return !!iterator.next() && !!iterator.next();
  544. },
  545. execute: () => {
  546. const widget = contextMenuWidget();
  547. if (!widget) {
  548. return;
  549. }
  550. const { id } = widget;
  551. const otherWidgets = toArray(shell.widgets('main')).filter(
  552. widget => widget.id !== id
  553. );
  554. closeWidgets(otherWidgets);
  555. }
  556. });
  557. contextMenu.addItem({
  558. command: CommandIDs.closeOtherTabs,
  559. selector: tabSelector,
  560. rank: 4
  561. });
  562. commands.addCommand(CommandIDs.closeRightTabs, {
  563. label: () => `Close Tabs to Right`,
  564. isEnabled: () =>
  565. !!contextMenuWidget() && widgetsRightOf(contextMenuWidget()!).length > 0,
  566. execute: () => {
  567. const widget = contextMenuWidget();
  568. if (!widget) {
  569. return;
  570. }
  571. closeWidgets(widgetsRightOf(widget));
  572. }
  573. });
  574. contextMenu.addItem({
  575. command: CommandIDs.closeRightTabs,
  576. selector: tabSelector,
  577. rank: 5
  578. });
  579. app.commands.addCommand(CommandIDs.toggleLeftArea, {
  580. label: () => 'Show Left Sidebar',
  581. execute: () => {
  582. if (shell.leftCollapsed) {
  583. shell.expandLeft();
  584. } else {
  585. shell.collapseLeft();
  586. if (shell.currentWidget) {
  587. shell.activateById(shell.currentWidget.id);
  588. }
  589. }
  590. },
  591. isToggled: () => !shell.leftCollapsed,
  592. isVisible: () => !shell.isEmpty('left')
  593. });
  594. app.commands.addCommand(CommandIDs.toggleRightArea, {
  595. label: () => 'Show Right Sidebar',
  596. execute: () => {
  597. if (shell.rightCollapsed) {
  598. shell.expandRight();
  599. } else {
  600. shell.collapseRight();
  601. if (shell.currentWidget) {
  602. shell.activateById(shell.currentWidget.id);
  603. }
  604. }
  605. },
  606. isToggled: () => !shell.rightCollapsed,
  607. isVisible: () => !shell.isEmpty('right')
  608. });
  609. app.commands.addCommand(CommandIDs.togglePresentationMode, {
  610. label: () => 'Presentation Mode',
  611. execute: () => {
  612. shell.presentationMode = !shell.presentationMode;
  613. },
  614. isToggled: () => shell.presentationMode,
  615. isVisible: () => true
  616. });
  617. app.commands.addCommand(CommandIDs.setMode, {
  618. isVisible: args => {
  619. const mode = args['mode'] as string;
  620. return mode === 'single-document' || mode === 'multiple-document';
  621. },
  622. execute: args => {
  623. const mode = args['mode'] as string;
  624. if (mode === 'single-document' || mode === 'multiple-document') {
  625. shell.mode = mode;
  626. return;
  627. }
  628. throw new Error(`Unsupported application shell mode: ${mode}`);
  629. }
  630. });
  631. app.commands.addCommand(CommandIDs.toggleMode, {
  632. label: 'Single-Document Mode',
  633. isToggled: () => shell.mode === 'single-document',
  634. execute: () => {
  635. const args =
  636. shell.mode === 'multiple-document'
  637. ? { mode: 'single-document' }
  638. : { mode: 'multiple-document' };
  639. return app.commands.execute(CommandIDs.setMode, args);
  640. }
  641. });
  642. if (palette) {
  643. palette.addItem({ command: CommandIDs.activateNextTab, category });
  644. palette.addItem({ command: CommandIDs.activatePreviousTab, category });
  645. palette.addItem({ command: CommandIDs.activateNextTabBar, category });
  646. palette.addItem({ command: CommandIDs.activatePreviousTabBar, category });
  647. palette.addItem({ command: CommandIDs.close, category });
  648. palette.addItem({ command: CommandIDs.closeAll, category });
  649. palette.addItem({ command: CommandIDs.closeOtherTabs, category });
  650. palette.addItem({ command: CommandIDs.closeRightTabs, category });
  651. palette.addItem({ command: CommandIDs.toggleLeftArea, category });
  652. palette.addItem({ command: CommandIDs.toggleRightArea, category });
  653. palette.addItem({ command: CommandIDs.togglePresentationMode, category });
  654. palette.addItem({ command: CommandIDs.toggleMode, category });
  655. }
  656. }
  657. /**
  658. * The default JupyterLab application shell.
  659. */
  660. const shell: JupyterFrontEndPlugin<ILabShell> = {
  661. id: '@jupyterlab/application-extension:shell',
  662. activate: (app: JupyterFrontEnd) => {
  663. if (!(app.shell instanceof LabShell)) {
  664. throw new Error(`${shell.id} did not find a LabShell instance.`);
  665. }
  666. return app.shell;
  667. },
  668. autoStart: true,
  669. provides: ILabShell
  670. };
  671. /**
  672. * The default JupyterLab application status provider.
  673. */
  674. const status: JupyterFrontEndPlugin<ILabStatus> = {
  675. id: '@jupyterlab/application-extension:status',
  676. activate: (app: JupyterFrontEnd) => {
  677. if (!(app instanceof JupyterLab)) {
  678. throw new Error(`${status.id} must be activated in JupyterLab.`);
  679. }
  680. return app.status;
  681. },
  682. autoStart: true,
  683. provides: ILabStatus
  684. };
  685. /**
  686. * The default JupyterLab application-specific information provider.
  687. *
  688. * #### Notes
  689. * This plugin should only be used by plugins that specifically need to access
  690. * JupyterLab application information, e.g., listing extensions that have been
  691. * loaded or deferred within JupyterLab.
  692. */
  693. const info: JupyterFrontEndPlugin<JupyterLab.IInfo> = {
  694. id: '@jupyterlab/application-extension:info',
  695. activate: (app: JupyterFrontEnd) => {
  696. if (!(app instanceof JupyterLab)) {
  697. throw new Error(`${info.id} must be activated in JupyterLab.`);
  698. }
  699. return app.info;
  700. },
  701. autoStart: true,
  702. provides: JupyterLab.IInfo
  703. };
  704. /**
  705. * The default JupyterLab paths dictionary provider.
  706. */
  707. const paths: JupyterFrontEndPlugin<JupyterFrontEnd.IPaths> = {
  708. id: '@jupyterlab/apputils-extension:paths',
  709. activate: (app: JupyterFrontEnd): JupyterFrontEnd.IPaths => {
  710. if (!(app instanceof JupyterLab)) {
  711. throw new Error(`${paths.id} must be activated in JupyterLab.`);
  712. }
  713. return app.paths;
  714. },
  715. autoStart: true,
  716. provides: JupyterFrontEnd.IPaths
  717. };
  718. /**
  719. * The default property inspector provider.
  720. */
  721. const propertyInspector: JupyterFrontEndPlugin<IPropertyInspectorProvider> = {
  722. id: '@jupyterlab/application-extension:property-inspector',
  723. autoStart: true,
  724. requires: [ILabShell],
  725. provides: IPropertyInspectorProvider,
  726. activate: (app: JupyterFrontEnd, labshell: ILabShell) => {
  727. const widget = new SideBarPropertyInspectorProvider(labshell);
  728. widget.title.icon = buildIcon;
  729. widget.title.caption = 'Property Inspector';
  730. widget.id = 'jp-property-inspector';
  731. labshell.add(widget, 'left');
  732. return widget;
  733. }
  734. };
  735. /**
  736. * Export the plugins as default.
  737. */
  738. const plugins: JupyterFrontEndPlugin<any>[] = [
  739. main,
  740. layout,
  741. router,
  742. tree,
  743. notfound,
  744. busy,
  745. sidebar,
  746. shell,
  747. status,
  748. info,
  749. paths,
  750. propertyInspector
  751. ];
  752. export default plugins;