index.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { each, find } from '@phosphor/algorithm';
  4. import { IDisposable } from '@phosphor/disposable';
  5. import { Menu, Widget } from '@phosphor/widgets';
  6. import {
  7. ILabShell,
  8. JupyterFrontEnd,
  9. JupyterFrontEndPlugin,
  10. IRouter
  11. } from '@jupyterlab/application';
  12. import { ICommandPalette, showDialog, Dialog } from '@jupyterlab/apputils';
  13. import { PageConfig, URLExt } from '@jupyterlab/coreutils';
  14. import { IInspector } from '@jupyterlab/inspector';
  15. import {
  16. IMainMenu,
  17. IMenuExtender,
  18. EditMenu,
  19. FileMenu,
  20. KernelMenu,
  21. MainMenu,
  22. RunMenu,
  23. SettingsMenu,
  24. ViewMenu,
  25. TabsMenu
  26. } from '@jupyterlab/mainmenu';
  27. import { ServerConnection } from '@jupyterlab/services';
  28. /**
  29. * A namespace for command IDs of semantic extension points.
  30. */
  31. export namespace CommandIDs {
  32. export const openEdit = 'editmenu:open';
  33. export const undo = 'editmenu:undo';
  34. export const redo = 'editmenu:redo';
  35. export const clearCurrent = 'editmenu:clear-current';
  36. export const clearAll = 'editmenu:clear-all';
  37. export const find = 'editmenu:find';
  38. export const goToLine = 'editmenu:go-to-line';
  39. export const openFile = 'filemenu:open';
  40. export const closeAndCleanup = 'filemenu:close-and-cleanup';
  41. export const createConsole = 'filemenu:create-console';
  42. export const shutdown = 'filemenu:shutdown';
  43. export const logout = 'filemenu:logout';
  44. export const openKernel = 'kernelmenu:open';
  45. export const interruptKernel = 'kernelmenu:interrupt';
  46. export const restartKernel = 'kernelmenu:restart';
  47. export const restartKernelAndClear = 'kernelmenu:restart-and-clear';
  48. export const changeKernel = 'kernelmenu:change';
  49. export const shutdownKernel = 'kernelmenu:shutdown';
  50. export const shutdownAllKernels = 'kernelmenu:shutdownAll';
  51. export const openView = 'viewmenu:open';
  52. export const wordWrap = 'viewmenu:word-wrap';
  53. export const lineNumbering = 'viewmenu:line-numbering';
  54. export const matchBrackets = 'viewmenu:match-brackets';
  55. export const openRun = 'runmenu:open';
  56. export const run = 'runmenu:run';
  57. export const runAll = 'runmenu:run-all';
  58. export const restartAndRunAll = 'runmenu:restart-and-run-all';
  59. export const runAbove = 'runmenu:run-above';
  60. export const runBelow = 'runmenu:run-below';
  61. export const openTabs = 'tabsmenu:open';
  62. export const activateById = 'tabsmenu:activate-by-id';
  63. export const activatePreviouslyUsedTab =
  64. 'tabsmenu:activate-previously-used-tab';
  65. export const openSettings = 'settingsmenu:open';
  66. export const openHelp = 'helpmenu:open';
  67. export const openFirst = 'mainmenu:open-first';
  68. }
  69. /**
  70. * A service providing an interface to the main menu.
  71. */
  72. const plugin: JupyterFrontEndPlugin<IMainMenu> = {
  73. id: '@jupyterlab/mainmenu-extension:plugin',
  74. requires: [ICommandPalette, IRouter],
  75. optional: [IInspector, ILabShell],
  76. provides: IMainMenu,
  77. activate: (
  78. app: JupyterFrontEnd,
  79. palette: ICommandPalette,
  80. router: IRouter,
  81. inspector: IInspector | null,
  82. labShell: ILabShell | null
  83. ): IMainMenu => {
  84. const { commands } = app;
  85. let menu = new MainMenu(commands);
  86. menu.id = 'jp-MainMenu';
  87. let logo = new Widget();
  88. logo.addClass('jp-MainAreaPortraitIcon');
  89. logo.addClass('jp-JupyterIcon');
  90. logo.id = 'jp-MainLogo';
  91. // Only add quit button if the back-end supports it by checking page config.
  92. let quitButton = PageConfig.getOption('quitButton');
  93. menu.fileMenu.quitEntry = quitButton === 'True';
  94. // Create the application menus.
  95. createEditMenu(app, menu.editMenu);
  96. createFileMenu(app, menu.fileMenu, router, inspector);
  97. createKernelMenu(app, menu.kernelMenu);
  98. createRunMenu(app, menu.runMenu);
  99. createSettingsMenu(app, menu.settingsMenu);
  100. createViewMenu(app, menu.viewMenu);
  101. // The tabs menu relies on lab shell functionality.
  102. if (labShell) {
  103. createTabsMenu(app, menu.tabsMenu, labShell);
  104. }
  105. // Create commands to open the main application menus.
  106. const activateMenu = (item: Menu) => {
  107. menu.activeMenu = item;
  108. menu.openActiveMenu();
  109. };
  110. commands.addCommand(CommandIDs.openEdit, {
  111. label: 'Open Edit Menu',
  112. execute: () => activateMenu(menu.editMenu.menu)
  113. });
  114. commands.addCommand(CommandIDs.openFile, {
  115. label: 'Open File Menu',
  116. execute: () => activateMenu(menu.fileMenu.menu)
  117. });
  118. commands.addCommand(CommandIDs.openKernel, {
  119. label: 'Open Kernel Menu',
  120. execute: () => activateMenu(menu.kernelMenu.menu)
  121. });
  122. commands.addCommand(CommandIDs.openRun, {
  123. label: 'Open Run Menu',
  124. execute: () => activateMenu(menu.runMenu.menu)
  125. });
  126. commands.addCommand(CommandIDs.openView, {
  127. label: 'Open View Menu',
  128. execute: () => activateMenu(menu.viewMenu.menu)
  129. });
  130. commands.addCommand(CommandIDs.openSettings, {
  131. label: 'Open Settings Menu',
  132. execute: () => activateMenu(menu.settingsMenu.menu)
  133. });
  134. commands.addCommand(CommandIDs.openTabs, {
  135. label: 'Open Tabs Menu',
  136. execute: () => activateMenu(menu.tabsMenu.menu)
  137. });
  138. commands.addCommand(CommandIDs.openHelp, {
  139. label: 'Open Help Menu',
  140. execute: () => activateMenu(menu.helpMenu.menu)
  141. });
  142. commands.addCommand(CommandIDs.openFirst, {
  143. label: 'Open First Menu',
  144. execute: () => {
  145. menu.activeIndex = 0;
  146. menu.openActiveMenu();
  147. }
  148. });
  149. // Add some of the commands defined here to the command palette.
  150. if (menu.fileMenu.quitEntry) {
  151. palette.addItem({
  152. command: CommandIDs.shutdown,
  153. category: 'Main Area'
  154. });
  155. palette.addItem({
  156. command: CommandIDs.logout,
  157. category: 'Main Area'
  158. });
  159. }
  160. palette.addItem({
  161. command: CommandIDs.shutdownAllKernels,
  162. category: 'Kernel Operations'
  163. });
  164. palette.addItem({
  165. command: CommandIDs.activatePreviouslyUsedTab,
  166. category: 'Main Area'
  167. });
  168. app.shell.add(logo, 'top');
  169. app.shell.add(menu, 'top');
  170. return menu;
  171. }
  172. };
  173. /**
  174. * Create the basic `Edit` menu.
  175. */
  176. export function createEditMenu(app: JupyterFrontEnd, menu: EditMenu): void {
  177. const commands = menu.menu.commands;
  178. // Add the undo/redo commands the the Edit menu.
  179. commands.addCommand(CommandIDs.undo, {
  180. label: 'Undo',
  181. isEnabled: Private.delegateEnabled(app, menu.undoers, 'undo'),
  182. execute: Private.delegateExecute(app, menu.undoers, 'undo')
  183. });
  184. commands.addCommand(CommandIDs.redo, {
  185. label: 'Redo',
  186. isEnabled: Private.delegateEnabled(app, menu.undoers, 'redo'),
  187. execute: Private.delegateExecute(app, menu.undoers, 'redo')
  188. });
  189. menu.addGroup(
  190. [{ command: CommandIDs.undo }, { command: CommandIDs.redo }],
  191. 0
  192. );
  193. // Add the clear commands to the Edit menu.
  194. commands.addCommand(CommandIDs.clearCurrent, {
  195. label: () => {
  196. const noun = Private.delegateLabel(app, menu.clearers, 'noun');
  197. const enabled = Private.delegateEnabled(
  198. app,
  199. menu.clearers,
  200. 'clearCurrent'
  201. )();
  202. return `Clear${enabled ? ` ${noun}` : ''}`;
  203. },
  204. isEnabled: Private.delegateEnabled(app, menu.clearers, 'clearCurrent'),
  205. execute: Private.delegateExecute(app, menu.clearers, 'clearCurrent')
  206. });
  207. commands.addCommand(CommandIDs.clearAll, {
  208. label: () => {
  209. const noun = Private.delegateLabel(app, menu.clearers, 'pluralNoun');
  210. const enabled = Private.delegateEnabled(app, menu.clearers, 'clearAll')();
  211. return `Clear All${enabled ? ` ${noun}` : ''}`;
  212. },
  213. isEnabled: Private.delegateEnabled(app, menu.clearers, 'clearAll'),
  214. execute: Private.delegateExecute(app, menu.clearers, 'clearAll')
  215. });
  216. menu.addGroup(
  217. [{ command: CommandIDs.clearCurrent }, { command: CommandIDs.clearAll }],
  218. 10
  219. );
  220. commands.addCommand(CommandIDs.goToLine, {
  221. label: 'Go to Line…',
  222. isEnabled: Private.delegateEnabled(app, menu.goToLiners, 'goToLine'),
  223. execute: Private.delegateExecute(app, menu.goToLiners, 'goToLine')
  224. });
  225. menu.addGroup([{ command: CommandIDs.goToLine }], 200);
  226. }
  227. /**
  228. * Create the basic `File` menu.
  229. */
  230. export function createFileMenu(
  231. app: JupyterFrontEnd,
  232. menu: FileMenu,
  233. router: IRouter,
  234. inspector: IInspector | null
  235. ): void {
  236. const commands = menu.menu.commands;
  237. // Add a delegator command for closing and cleaning up an activity.
  238. commands.addCommand(CommandIDs.closeAndCleanup, {
  239. label: () => {
  240. const action = Private.delegateLabel(
  241. app,
  242. menu.closeAndCleaners,
  243. 'action'
  244. );
  245. const name = Private.delegateLabel(app, menu.closeAndCleaners, 'name');
  246. return `Close and ${action ? ` ${action} ${name}` : 'Shutdown'}`;
  247. },
  248. isEnabled: Private.delegateEnabled(
  249. app,
  250. menu.closeAndCleaners,
  251. 'closeAndCleanup'
  252. ),
  253. execute: Private.delegateExecute(
  254. app,
  255. menu.closeAndCleaners,
  256. 'closeAndCleanup'
  257. )
  258. });
  259. // Add a delegator command for creating a console for an activity.
  260. commands.addCommand(CommandIDs.createConsole, {
  261. label: () => {
  262. const name = Private.delegateLabel(app, menu.consoleCreators, 'name');
  263. const label = `New Console for ${name ? name : 'Activity'}`;
  264. return label;
  265. },
  266. isEnabled: Private.delegateEnabled(
  267. app,
  268. menu.consoleCreators,
  269. 'createConsole'
  270. ),
  271. execute: Private.delegateExecute(app, menu.consoleCreators, 'createConsole')
  272. });
  273. commands.addCommand(CommandIDs.shutdown, {
  274. label: 'Shut Down',
  275. caption: 'Shut down JupyterLab',
  276. execute: () => {
  277. return showDialog({
  278. title: 'Shutdown confirmation',
  279. body: 'Please confirm you want to shut down JupyterLab.',
  280. buttons: [
  281. Dialog.cancelButton(),
  282. Dialog.warnButton({ label: 'Shut Down' })
  283. ]
  284. }).then(result => {
  285. if (result.button.accept) {
  286. let setting = ServerConnection.makeSettings();
  287. let apiURL = URLExt.join(setting.baseUrl, 'api/shutdown');
  288. return ServerConnection.makeRequest(
  289. apiURL,
  290. { method: 'POST' },
  291. setting
  292. )
  293. .then(result => {
  294. if (result.ok) {
  295. // Close this window if the shutdown request has been successful
  296. let body = document.createElement('div');
  297. body.innerHTML = `<p>You have shut down the Jupyter server. You can now close this tab.</p>
  298. <p>To use JupyterLab again, you will need to relaunch it.</p>`;
  299. void showDialog({
  300. title: 'Server stopped',
  301. body: new Widget({ node: body }),
  302. buttons: []
  303. });
  304. window.close();
  305. } else {
  306. throw new ServerConnection.ResponseError(result);
  307. }
  308. })
  309. .catch(data => {
  310. throw new ServerConnection.NetworkError(data);
  311. });
  312. }
  313. });
  314. }
  315. });
  316. commands.addCommand(CommandIDs.logout, {
  317. label: 'Log Out',
  318. caption: 'Log out of JupyterLab',
  319. execute: () => {
  320. router.navigate('/logout', { hard: true });
  321. }
  322. });
  323. // Add the new group
  324. const newGroup = [
  325. { type: 'submenu' as Menu.ItemType, submenu: menu.newMenu.menu },
  326. { command: 'filebrowser:create-main-launcher' }
  327. ];
  328. const newViewGroup = [
  329. { command: 'docmanager:clone' },
  330. { command: CommandIDs.createConsole },
  331. inspector ? { command: 'inspector:open' } : null,
  332. { command: 'docmanager:open-direct' }
  333. ].filter(item => !!item);
  334. // Add the close group
  335. const closeGroup = [
  336. 'application:close',
  337. 'filemenu:close-and-cleanup',
  338. 'application:close-all'
  339. ].map(command => {
  340. return { command };
  341. });
  342. // Add save group.
  343. const saveGroup = [
  344. 'docmanager:save',
  345. 'docmanager:save-as',
  346. 'docmanager:save-all'
  347. ].map(command => {
  348. return { command };
  349. });
  350. // Add the re group.
  351. const reGroup = [
  352. 'docmanager:reload',
  353. 'docmanager:restore-checkpoint',
  354. 'docmanager:rename'
  355. ].map(command => {
  356. return { command };
  357. });
  358. // Add the quit group.
  359. const quitGroup = [
  360. { command: 'filemenu:logout' },
  361. { command: 'filemenu:shutdown' }
  362. ];
  363. const printGroup = [{ command: 'apputils:print' }];
  364. menu.addGroup(newGroup, 0);
  365. menu.addGroup(newViewGroup, 1);
  366. menu.addGroup(closeGroup, 2);
  367. menu.addGroup(saveGroup, 3);
  368. menu.addGroup(reGroup, 4);
  369. menu.addGroup(printGroup, 98);
  370. if (menu.quitEntry) {
  371. menu.addGroup(quitGroup, 99);
  372. }
  373. }
  374. /**
  375. * Create the basic `Kernel` menu.
  376. */
  377. export function createKernelMenu(app: JupyterFrontEnd, menu: KernelMenu): void {
  378. const commands = menu.menu.commands;
  379. commands.addCommand(CommandIDs.interruptKernel, {
  380. label: 'Interrupt Kernel',
  381. isEnabled: Private.delegateEnabled(
  382. app,
  383. menu.kernelUsers,
  384. 'interruptKernel'
  385. ),
  386. execute: Private.delegateExecute(app, menu.kernelUsers, 'interruptKernel')
  387. });
  388. commands.addCommand(CommandIDs.restartKernel, {
  389. label: 'Restart Kernel…',
  390. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'restartKernel'),
  391. execute: Private.delegateExecute(app, menu.kernelUsers, 'restartKernel')
  392. });
  393. commands.addCommand(CommandIDs.restartKernelAndClear, {
  394. label: () => {
  395. const noun = Private.delegateLabel(app, menu.kernelUsers, 'noun');
  396. const enabled = Private.delegateEnabled(
  397. app,
  398. menu.kernelUsers,
  399. 'restartKernelAndClear'
  400. )();
  401. return `Restart Kernel and Clear${enabled ? ` ${noun}` : ''}…`;
  402. },
  403. isEnabled: Private.delegateEnabled(
  404. app,
  405. menu.kernelUsers,
  406. 'restartKernelAndClear'
  407. ),
  408. execute: Private.delegateExecute(
  409. app,
  410. menu.kernelUsers,
  411. 'restartKernelAndClear'
  412. )
  413. });
  414. commands.addCommand(CommandIDs.changeKernel, {
  415. label: 'Change Kernel…',
  416. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'changeKernel'),
  417. execute: Private.delegateExecute(app, menu.kernelUsers, 'changeKernel')
  418. });
  419. commands.addCommand(CommandIDs.shutdownKernel, {
  420. label: 'Shut Down Kernel',
  421. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'shutdownKernel'),
  422. execute: Private.delegateExecute(app, menu.kernelUsers, 'shutdownKernel')
  423. });
  424. commands.addCommand(CommandIDs.shutdownAllKernels, {
  425. label: 'Shut Down All Kernels…',
  426. isEnabled: () => {
  427. return app.serviceManager.sessions.running().next() !== undefined;
  428. },
  429. execute: () => {
  430. return showDialog({
  431. title: 'Shut Down All?',
  432. body: 'Shut down all kernels?',
  433. buttons: [
  434. Dialog.cancelButton(),
  435. Dialog.warnButton({ label: 'SHUT DOWN ALL' })
  436. ]
  437. }).then(result => {
  438. if (result.button.accept) {
  439. return app.serviceManager.sessions.shutdownAll();
  440. }
  441. });
  442. }
  443. });
  444. const restartGroup = [
  445. CommandIDs.restartKernel,
  446. CommandIDs.restartKernelAndClear,
  447. CommandIDs.restartAndRunAll
  448. ].map(command => {
  449. return { command };
  450. });
  451. menu.addGroup([{ command: CommandIDs.interruptKernel }], 0);
  452. menu.addGroup(restartGroup, 1);
  453. menu.addGroup(
  454. [
  455. { command: CommandIDs.shutdownKernel },
  456. { command: CommandIDs.shutdownAllKernels }
  457. ],
  458. 2
  459. );
  460. menu.addGroup([{ command: CommandIDs.changeKernel }], 3);
  461. }
  462. /**
  463. * Create the basic `View` menu.
  464. */
  465. export function createViewMenu(app: JupyterFrontEnd, menu: ViewMenu): void {
  466. const commands = menu.menu.commands;
  467. commands.addCommand(CommandIDs.lineNumbering, {
  468. label: 'Show Line Numbers',
  469. isEnabled: Private.delegateEnabled(
  470. app,
  471. menu.editorViewers,
  472. 'toggleLineNumbers'
  473. ),
  474. isToggled: Private.delegateToggled(
  475. app,
  476. menu.editorViewers,
  477. 'lineNumbersToggled'
  478. ),
  479. execute: Private.delegateExecute(
  480. app,
  481. menu.editorViewers,
  482. 'toggleLineNumbers'
  483. )
  484. });
  485. commands.addCommand(CommandIDs.matchBrackets, {
  486. label: 'Match Brackets',
  487. isEnabled: Private.delegateEnabled(
  488. app,
  489. menu.editorViewers,
  490. 'toggleMatchBrackets'
  491. ),
  492. isToggled: Private.delegateToggled(
  493. app,
  494. menu.editorViewers,
  495. 'matchBracketsToggled'
  496. ),
  497. execute: Private.delegateExecute(
  498. app,
  499. menu.editorViewers,
  500. 'toggleMatchBrackets'
  501. )
  502. });
  503. commands.addCommand(CommandIDs.wordWrap, {
  504. label: 'Wrap Words',
  505. isEnabled: Private.delegateEnabled(
  506. app,
  507. menu.editorViewers,
  508. 'toggleWordWrap'
  509. ),
  510. isToggled: Private.delegateToggled(
  511. app,
  512. menu.editorViewers,
  513. 'wordWrapToggled'
  514. ),
  515. execute: Private.delegateExecute(app, menu.editorViewers, 'toggleWordWrap')
  516. });
  517. menu.addGroup(
  518. [
  519. { command: 'application:toggle-left-area' },
  520. { command: 'application:toggle-right-area' }
  521. ],
  522. 0
  523. );
  524. const editorViewerGroup = [
  525. CommandIDs.lineNumbering,
  526. CommandIDs.matchBrackets,
  527. CommandIDs.wordWrap
  528. ].map(command => {
  529. return { command };
  530. });
  531. menu.addGroup(editorViewerGroup, 10);
  532. // Add the command for toggling single-document mode.
  533. menu.addGroup(
  534. [
  535. { command: 'application:toggle-presentation-mode' },
  536. { command: 'application:toggle-mode' }
  537. ],
  538. 1000
  539. );
  540. }
  541. /**
  542. * Create the basic `Run` menu.
  543. */
  544. export function createRunMenu(app: JupyterFrontEnd, menu: RunMenu): void {
  545. const commands = menu.menu.commands;
  546. commands.addCommand(CommandIDs.run, {
  547. label: () => {
  548. const noun = Private.delegateLabel(app, menu.codeRunners, 'noun');
  549. const enabled = Private.delegateEnabled(app, menu.codeRunners, 'run')();
  550. return `Run Selected${enabled ? ` ${noun}` : ''}`;
  551. },
  552. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'run'),
  553. execute: Private.delegateExecute(app, menu.codeRunners, 'run')
  554. });
  555. commands.addCommand(CommandIDs.runAll, {
  556. label: () => {
  557. const noun = Private.delegateLabel(app, menu.codeRunners, 'noun');
  558. const enabled = Private.delegateEnabled(
  559. app,
  560. menu.codeRunners,
  561. 'runAll'
  562. )();
  563. return `Run All${enabled ? ` ${noun}` : ''}`;
  564. },
  565. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'runAll'),
  566. execute: Private.delegateExecute(app, menu.codeRunners, 'runAll')
  567. });
  568. commands.addCommand(CommandIDs.restartAndRunAll, {
  569. label: () => {
  570. const noun = Private.delegateLabel(app, menu.codeRunners, 'noun');
  571. const enabled = Private.delegateEnabled(
  572. app,
  573. menu.codeRunners,
  574. 'restartAndRunAll'
  575. )();
  576. return `Restart Kernel and Run All${enabled ? ` ${noun}` : ''}…`;
  577. },
  578. isEnabled: Private.delegateEnabled(
  579. app,
  580. menu.codeRunners,
  581. 'restartAndRunAll'
  582. ),
  583. execute: Private.delegateExecute(app, menu.codeRunners, 'restartAndRunAll')
  584. });
  585. const runAllGroup = [CommandIDs.runAll, CommandIDs.restartAndRunAll].map(
  586. command => {
  587. return { command };
  588. }
  589. );
  590. menu.addGroup([{ command: CommandIDs.run }], 0);
  591. menu.addGroup(runAllGroup, 999);
  592. }
  593. /**
  594. * Create the basic `Settings` menu.
  595. */
  596. export function createSettingsMenu(
  597. _: JupyterFrontEnd,
  598. menu: SettingsMenu
  599. ): void {
  600. menu.addGroup([{ command: 'settingeditor:open' }], 1000);
  601. }
  602. /**
  603. * Create the basic `Tabs` menu.
  604. */
  605. export function createTabsMenu(
  606. app: JupyterFrontEnd,
  607. menu: TabsMenu,
  608. labShell: ILabShell | null
  609. ): void {
  610. const commands = app.commands;
  611. // Add commands for cycling the active tabs.
  612. menu.addGroup(
  613. [
  614. { command: 'application:activate-next-tab' },
  615. { command: 'application:activate-previous-tab' },
  616. { command: CommandIDs.activatePreviouslyUsedTab }
  617. ],
  618. 0
  619. );
  620. // A list of the active tabs in the main area.
  621. const tabGroup: Menu.IItemOptions[] = [];
  622. // A disposable for getting rid of the out-of-date tabs list.
  623. let disposable: IDisposable;
  624. // Command to activate a widget by id.
  625. commands.addCommand(CommandIDs.activateById, {
  626. label: args => {
  627. const id = args['id'] || '';
  628. const widget = find(app.shell.widgets('main'), w => w.id === id);
  629. return (widget && widget.title.label) || '';
  630. },
  631. isToggled: args => {
  632. const id = args['id'] || '';
  633. return app.shell.currentWidget && app.shell.currentWidget.id === id;
  634. },
  635. execute: args => app.shell.activateById((args['id'] as string) || '')
  636. });
  637. let previousId = '';
  638. // Command to toggle between the current
  639. // tab and the last modified tab.
  640. commands.addCommand(CommandIDs.activatePreviouslyUsedTab, {
  641. label: 'Activate Previously Used Tab',
  642. isEnabled: () => !!previousId,
  643. execute: () => commands.execute(CommandIDs.activateById, { id: previousId })
  644. });
  645. if (labShell) {
  646. void app.restored.then(() => {
  647. // Iterate over the current widgets in the
  648. // main area, and add them to the tab group
  649. // of the menu.
  650. const populateTabs = () => {
  651. // remove the previous tab list
  652. if (disposable && !disposable.isDisposed) {
  653. disposable.dispose();
  654. }
  655. tabGroup.length = 0;
  656. let isPreviouslyUsedTabAttached = false;
  657. each(app.shell.widgets('main'), widget => {
  658. if (widget.id === previousId) {
  659. isPreviouslyUsedTabAttached = true;
  660. }
  661. tabGroup.push({
  662. command: CommandIDs.activateById,
  663. args: { id: widget.id }
  664. });
  665. });
  666. disposable = menu.addGroup(tabGroup, 1);
  667. previousId = isPreviouslyUsedTabAttached ? previousId : '';
  668. };
  669. populateTabs();
  670. labShell.layoutModified.connect(() => {
  671. populateTabs();
  672. });
  673. // Update the ID of the previous active tab if a new tab is selected.
  674. labShell.currentChanged.connect((_, args) => {
  675. let widget = args.oldValue;
  676. if (!widget) {
  677. return;
  678. }
  679. previousId = widget.id;
  680. });
  681. });
  682. }
  683. }
  684. export default plugin;
  685. /**
  686. * A namespace for Private data.
  687. */
  688. namespace Private {
  689. /**
  690. * Return the first value of the iterable that satisfies the predicate
  691. * function.
  692. */
  693. function find<T>(
  694. it: Iterable<T>,
  695. predicate: (value: T) => boolean
  696. ): T | undefined {
  697. for (let value of it) {
  698. if (predicate(value)) {
  699. return value;
  700. }
  701. }
  702. return undefined;
  703. }
  704. /**
  705. * A utility function that delegates a portion of a label to an IMenuExtender.
  706. */
  707. export function delegateLabel<E extends IMenuExtender<Widget>>(
  708. app: JupyterFrontEnd,
  709. s: Set<E>,
  710. label: keyof E
  711. ): string {
  712. let widget = app.shell.currentWidget;
  713. const extender = find(s, value => value.tracker.has(widget));
  714. if (!extender) {
  715. return '';
  716. }
  717. // Coerce the result to be a string. When Typedoc is updated to use
  718. // Typescript 2.8, we can possibly use conditional types to get Typescript
  719. // to recognize this is a string.
  720. return (extender[label] as any) as string;
  721. }
  722. /**
  723. * A utility function that delegates command execution
  724. * to an IMenuExtender.
  725. */
  726. export function delegateExecute<E extends IMenuExtender<Widget>>(
  727. app: JupyterFrontEnd,
  728. s: Set<E>,
  729. executor: keyof E
  730. ): () => Promise<any> {
  731. return () => {
  732. let widget = app.shell.currentWidget;
  733. const extender = find(s, value => value.tracker.has(widget));
  734. if (!extender) {
  735. return Promise.resolve(void 0);
  736. }
  737. // Coerce the result to be a function. When Typedoc is updated to use
  738. // Typescript 2.8, we can possibly use conditional types to get Typescript
  739. // to recognize this is a function.
  740. let f = (extender[executor] as any) as (w: Widget) => Promise<any>;
  741. return f(widget);
  742. };
  743. }
  744. /**
  745. * A utility function that delegates whether a command is enabled
  746. * to an IMenuExtender.
  747. */
  748. export function delegateEnabled<E extends IMenuExtender<Widget>>(
  749. app: JupyterFrontEnd,
  750. s: Set<E>,
  751. executor: keyof E
  752. ): () => boolean {
  753. return () => {
  754. let widget = app.shell.currentWidget;
  755. const extender = find(s, value => value.tracker.has(widget));
  756. return (
  757. !!extender &&
  758. !!extender[executor] &&
  759. (extender.isEnabled ? extender.isEnabled(widget) : true)
  760. );
  761. };
  762. }
  763. /**
  764. * A utility function that delegates whether a command is toggled
  765. * for an IMenuExtender.
  766. */
  767. export function delegateToggled<E extends IMenuExtender<Widget>>(
  768. app: JupyterFrontEnd,
  769. s: Set<E>,
  770. toggled: keyof E
  771. ): () => boolean {
  772. return () => {
  773. let widget = app.shell.currentWidget;
  774. const extender = find(s, value => value.tracker.has(widget));
  775. // Coerce extender[toggled] to be a function. When Typedoc is updated to use
  776. // Typescript 2.8, we can possibly use conditional types to get Typescript
  777. // to recognize this is a function.
  778. return (
  779. !!extender &&
  780. !!extender[toggled] &&
  781. !!((extender[toggled] as any) as (w: Widget) => () => boolean)(widget)
  782. );
  783. };
  784. }
  785. }