toolbar.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. IIterator, find, map
  5. } from '@phosphor/algorithm';
  6. import {
  7. CommandRegistry
  8. } from '@phosphor/commands';
  9. import {
  10. Message
  11. } from '@phosphor/messaging';
  12. import {
  13. AttachedProperty
  14. } from '@phosphor/properties';
  15. import {
  16. PanelLayout, Widget
  17. } from '@phosphor/widgets';
  18. import {
  19. IClientSession, Styling
  20. } from '.';
  21. /**
  22. * The class name added to toolbars.
  23. */
  24. const TOOLBAR_CLASS = 'jp-Toolbar';
  25. /**
  26. * The class name added to toolbar items.
  27. */
  28. const TOOLBAR_ITEM_CLASS = 'jp-Toolbar-item';
  29. /**
  30. * The class name added to toolbar buttons.
  31. */
  32. const TOOLBAR_BUTTON_CLASS = 'jp-Toolbar-button';
  33. /**
  34. * The class name added to a pressed button.
  35. */
  36. const TOOLBAR_PRESSED_CLASS = 'jp-mod-pressed';
  37. /**
  38. * The class name added to toolbar interrupt button.
  39. */
  40. const TOOLBAR_INTERRUPT_CLASS = 'jp-StopIcon';
  41. /**
  42. * The class name added to toolbar restart button.
  43. */
  44. const TOOLBAR_RESTART_CLASS = 'jp-RefreshIcon';
  45. /**
  46. * The class name added to toolbar kernel name text.
  47. */
  48. const TOOLBAR_KERNEL_NAME_CLASS = 'jp-Toolbar-kernelName';
  49. /**
  50. * The class name added to toolbar spacer.
  51. */
  52. const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
  53. /**
  54. * The class name added to toolbar kernel status icon.
  55. */
  56. const TOOLBAR_KERNEL_STATUS_CLASS = 'jp-Toolbar-kernelStatus';
  57. /**
  58. * The class name added to a busy kernel indicator.
  59. */
  60. const TOOLBAR_BUSY_CLASS = 'jp-FilledCircleIcon';
  61. const TOOLBAR_IDLE_CLASS = 'jp-CircleIcon';
  62. /**
  63. * A class which provides a toolbar widget.
  64. */
  65. export
  66. class Toolbar<T extends Widget> extends Widget {
  67. /**
  68. * Construct a new toolbar widget.
  69. */
  70. constructor() {
  71. super();
  72. this.addClass(TOOLBAR_CLASS);
  73. this.layout = new PanelLayout();
  74. }
  75. /**
  76. * Get an iterator over the ordered toolbar item names.
  77. *
  78. * @returns An iterator over the toolbar item names.
  79. */
  80. names(): IIterator<string> {
  81. let layout = this.layout as PanelLayout;
  82. return map(layout.widgets, widget => {
  83. return Private.nameProperty.get(widget);
  84. });
  85. }
  86. /**
  87. * Add an item to the end of the toolbar.
  88. *
  89. * @param name - The name of the widget to add to the toolbar.
  90. *
  91. * @param widget - The widget to add to the toolbar.
  92. *
  93. * @param index - The optional name of the item to insert after.
  94. *
  95. * @returns Whether the item was added to toolbar. Returns false if
  96. * an item of the same name is already in the toolbar.
  97. *
  98. * #### Notes
  99. * The item can be removed from the toolbar by setting its parent to `null`.
  100. */
  101. addItem(name: string, widget: T): boolean {
  102. let layout = this.layout as PanelLayout;
  103. return this.insertItem(layout.widgets.length, name, widget);
  104. }
  105. /**
  106. * Insert an item into the toolbar at the specified index.
  107. *
  108. * @param index - The index at which to insert the item.
  109. *
  110. * @param name - The name of the item.
  111. *
  112. * @param widget - The widget to add.
  113. *
  114. * @returns Whether the item was added to the toolbar. Returns false if
  115. * an item of the same name is already in the toolbar.
  116. *
  117. * #### Notes
  118. * The index will be clamped to the bounds of the items.
  119. * The item can be removed from the toolbar by setting its parent to `null`.
  120. */
  121. insertItem(index: number, name: string, widget: T): boolean {
  122. let existing = find(this.names(), value => value === name);
  123. if (existing) {
  124. return false;
  125. }
  126. widget.addClass(TOOLBAR_ITEM_CLASS);
  127. let layout = this.layout as PanelLayout;
  128. layout.insertWidget(index, widget);
  129. Private.nameProperty.set(widget, name);
  130. return true;
  131. }
  132. /**
  133. * Handle the DOM events for the widget.
  134. *
  135. * @param event - The DOM event sent to the widget.
  136. *
  137. * #### Notes
  138. * This method implements the DOM `EventListener` interface and is
  139. * called in response to events on the dock panel's node. It should
  140. * not be called directly by user code.
  141. */
  142. handleEvent(event: Event): void {
  143. switch (event.type) {
  144. case 'click':
  145. if (!this.node.contains(document.activeElement) && this.parent) {
  146. this.parent.activate();
  147. }
  148. break;
  149. default:
  150. break;
  151. }
  152. }
  153. /**
  154. * Handle `after-attach` messages for the widget.
  155. */
  156. protected onAfterAttach(msg: Message): void {
  157. this.node.addEventListener('click', this);
  158. }
  159. /**
  160. * Handle `before-detach` messages for the widget.
  161. */
  162. protected onBeforeDetach(msg: Message): void {
  163. this.node.removeEventListener('click', this);
  164. }
  165. }
  166. /**
  167. * The namespace for Toolbar class statics.
  168. */
  169. export
  170. namespace Toolbar {
  171. /**
  172. * Create a toolbar item for a command.
  173. *
  174. * Notes:
  175. * If the command has an icon label it will be added to the button.
  176. * If there is no icon label, and no icon class, the main label will
  177. * be added.
  178. */
  179. export
  180. function createFromCommand(commands: CommandRegistry, id: string): ToolbarButton {
  181. let button = new ToolbarButton({
  182. onClick: () => { commands.execute(id); },
  183. className: Private.commandClassName(commands, id),
  184. tooltip: Private.commandTooltip(commands, id),
  185. });
  186. let oldClasses = Private.commandClassName(commands, id).split(/\s/);
  187. (button.node as HTMLButtonElement).disabled = !commands.isEnabled(id);
  188. Private.setNodeContentFromCommand(button.node, commands, id);
  189. // Ensure that we pick up relevant changes to the command:
  190. function onChange(sender: CommandRegistry, args: CommandRegistry.ICommandChangedArgs) {
  191. if (args.id !== id) {
  192. return; // Not our command
  193. }
  194. if (args.type === 'removed') {
  195. // Dispose of button
  196. button.dispose();
  197. } else if (args.type === 'changed') {
  198. // Update all fields (onClick is already indirected)
  199. let newClasses = Private.commandClassName(sender, id).split(/\s/);
  200. for (let cls of oldClasses) {
  201. if (cls && newClasses.indexOf(cls) === -1) {
  202. button.removeClass(cls);
  203. }
  204. }
  205. for (let cls of newClasses) {
  206. if (cls && oldClasses.indexOf(cls) === -1) {
  207. button.addClass(cls);
  208. }
  209. }
  210. oldClasses = newClasses;
  211. button.node.title = Private.commandTooltip(sender, id);
  212. Private.setNodeContentFromCommand(button.node, sender, id);
  213. (button.node as HTMLButtonElement).disabled = !sender.isEnabled(id);
  214. }
  215. }
  216. commands.commandChanged.connect(onChange, button);
  217. return button;
  218. }
  219. /**
  220. * Create an interrupt toolbar item.
  221. */
  222. export
  223. function createInterruptButton(session: IClientSession): ToolbarButton {
  224. return new ToolbarButton({
  225. className: TOOLBAR_INTERRUPT_CLASS,
  226. onClick: () => {
  227. if (session.kernel) {
  228. session.kernel.interrupt();
  229. }
  230. },
  231. tooltip: 'Interrupt the kernel'
  232. });
  233. }
  234. /**
  235. * Create a restart toolbar item.
  236. */
  237. export
  238. function createRestartButton(session: IClientSession): ToolbarButton {
  239. return new ToolbarButton({
  240. className: TOOLBAR_RESTART_CLASS,
  241. onClick: () => {
  242. session.restart();
  243. },
  244. tooltip: 'Restart the kernel'
  245. });
  246. }
  247. /**
  248. * Create a toolbar spacer item.
  249. *
  250. * #### Notes
  251. * It is a flex spacer that separates the left toolbar items
  252. * from the right toolbar items.
  253. */
  254. export
  255. function createSpacerItem(): Widget {
  256. return new Private.Spacer();
  257. }
  258. /**
  259. * Create a kernel name indicator item.
  260. *
  261. * #### Notes
  262. * It will display the `'display_name`' of the current kernel,
  263. * or `'No Kernel!'` if there is no kernel.
  264. * It can handle a change in context or kernel.
  265. */
  266. export
  267. function createKernelNameItem(session: IClientSession): ToolbarButton {
  268. return new Private.KernelName(session);
  269. }
  270. /**
  271. * Create a kernel status indicator item.
  272. *
  273. * #### Notes
  274. * It show display a busy status if the kernel status is
  275. * not idle.
  276. * It will show the current status in the node title.
  277. * It can handle a change to the context or the kernel.
  278. */
  279. export
  280. function createKernelStatusItem(session: IClientSession): Widget {
  281. return new Private.KernelStatus(session);
  282. }
  283. }
  284. /**
  285. * A widget which acts as a button in a toolbar.
  286. */
  287. export
  288. class ToolbarButton extends Widget {
  289. /**
  290. * Construct a new toolbar button.
  291. */
  292. constructor(options: ToolbarButton.IOptions = {}) {
  293. super({ node: document.createElement('button') });
  294. Styling.styleNodeByTag(this.node, 'button');
  295. this.addClass(TOOLBAR_BUTTON_CLASS);
  296. this._onClick = options.onClick || Private.noOp;
  297. if (options.className) {
  298. for (let extra of options.className.split(/\s/)) {
  299. this.addClass(extra);
  300. }
  301. }
  302. this.node.title = options.tooltip || '';
  303. }
  304. /**
  305. * Handle the DOM events for the widget.
  306. *
  307. * @param event - The DOM event sent to the widget.
  308. *
  309. * #### Notes
  310. * This method implements the DOM `EventListener` interface and is
  311. * called in response to events on the dock panel's node. It should
  312. * not be called directly by user code.
  313. */
  314. handleEvent(event: Event): void {
  315. switch (event.type) {
  316. case 'click':
  317. if ((event as MouseEvent).button === 0) {
  318. this._onClick();
  319. }
  320. break;
  321. case 'mousedown':
  322. this.addClass(TOOLBAR_PRESSED_CLASS);
  323. break;
  324. case 'mouseup':
  325. case 'mouseout':
  326. this.removeClass(TOOLBAR_PRESSED_CLASS);
  327. break;
  328. default:
  329. break;
  330. }
  331. }
  332. /**
  333. * Handle `after-attach` messages for the widget.
  334. */
  335. protected onAfterAttach(msg: Message): void {
  336. this.node.addEventListener('click', this);
  337. this.node.addEventListener('mousedown', this);
  338. this.node.addEventListener('mouseup', this);
  339. this.node.addEventListener('mouseout', this);
  340. }
  341. /**
  342. * Handle `before-detach` messages for the widget.
  343. */
  344. protected onBeforeDetach(msg: Message): void {
  345. this.node.removeEventListener('click', this);
  346. this.node.removeEventListener('mousedown', this);
  347. this.node.removeEventListener('mouseup', this);
  348. this.node.removeEventListener('mouseout', this);
  349. }
  350. private _onClick: () => void;
  351. }
  352. /**
  353. * A namespace for `ToolbarButton` statics.
  354. */
  355. export
  356. namespace ToolbarButton {
  357. /**
  358. * The options used to construct a toolbar button.
  359. */
  360. export
  361. interface IOptions {
  362. /**
  363. * The callback for a click event.
  364. */
  365. onClick?: () => void;
  366. /**
  367. * The class name added to the button.
  368. */
  369. className?: string;
  370. /**
  371. * The tooltip added to the button node.
  372. */
  373. tooltip?: string;
  374. }
  375. }
  376. /**
  377. * A namespace for private data.
  378. */
  379. namespace Private {
  380. /**
  381. * An attached property for the name of a toolbar item.
  382. */
  383. export
  384. const nameProperty = new AttachedProperty<Widget, string>({
  385. name: 'name',
  386. create: () => ''
  387. });
  388. /**
  389. * ToolbarButton tooltip formatter for a command.
  390. */
  391. export
  392. function commandTooltip(commands: CommandRegistry, id: string): string {
  393. return commands.caption(id);
  394. }
  395. /**
  396. * A no-op function.
  397. */
  398. export
  399. function noOp() { /* no-op */ }
  400. /**
  401. * Get the class names for a command based ToolBarButton
  402. */
  403. export
  404. function commandClassName(commands: CommandRegistry, id: string): string {
  405. let name = commands.className(id);
  406. // Add the boolean state classes.
  407. if (commands.isToggled(id)) {
  408. name += ' p-mod-toggled';
  409. }
  410. if (!commands.isVisible(id)) {
  411. name += ' p-mod-hidden';
  412. }
  413. return name;
  414. }
  415. /**
  416. * Fill the node of a command based ToolBarButton.
  417. */
  418. export
  419. function setNodeContentFromCommand(node: HTMLElement, commands: CommandRegistry, id: string): void {
  420. let iconClass = commands.iconClass(id);
  421. let iconLabel = commands.iconLabel(id);
  422. node.innerHTML = '';
  423. if (iconClass || iconLabel) {
  424. let icon = document.createElement('div');
  425. icon.innerText = commands.iconLabel(id);
  426. icon.className += ` ${iconClass}`;
  427. node.appendChild(icon);
  428. } else {
  429. node.innerText = commands.label(id);
  430. }
  431. }
  432. /**
  433. * A spacer widget.
  434. */
  435. export
  436. class Spacer extends Widget {
  437. /**
  438. * Construct a new spacer widget.
  439. */
  440. constructor() {
  441. super();
  442. this.addClass(TOOLBAR_SPACER_CLASS);
  443. }
  444. }
  445. /**
  446. * A kernel name widget.
  447. */
  448. export
  449. class KernelName extends ToolbarButton {
  450. /**
  451. * Construct a new kernel name widget.
  452. */
  453. constructor(session: IClientSession) {
  454. super({
  455. className: TOOLBAR_KERNEL_NAME_CLASS,
  456. onClick: () => {
  457. session.selectKernel();
  458. },
  459. tooltip: 'Switch kernel'
  460. });
  461. this._onKernelChanged(session);
  462. session.kernelChanged.connect(this._onKernelChanged, this);
  463. }
  464. /**
  465. * Update the text of the kernel name item.
  466. */
  467. _onKernelChanged(session: IClientSession): void {
  468. this.node.textContent = session.kernelDisplayName;
  469. }
  470. }
  471. /**
  472. * A toolbar item that displays kernel status.
  473. */
  474. export
  475. class KernelStatus extends Widget {
  476. /**
  477. * Construct a new kernel status widget.
  478. */
  479. constructor(session: IClientSession) {
  480. super();
  481. this.addClass(TOOLBAR_KERNEL_STATUS_CLASS);
  482. this._onStatusChanged(session);
  483. session.statusChanged.connect(this._onStatusChanged, this);
  484. }
  485. /**
  486. * Handle a status on a kernel.
  487. */
  488. private _onStatusChanged(session: IClientSession) {
  489. if (this.isDisposed) {
  490. return;
  491. }
  492. let status = session.status;
  493. this.toggleClass(TOOLBAR_IDLE_CLASS, status === 'idle');
  494. this.toggleClass(TOOLBAR_BUSY_CLASS, status !== 'idle');
  495. let title = 'Kernel ' + status[0].toUpperCase() + status.slice(1);
  496. this.node.title = title;
  497. }
  498. }
  499. }