toolbar.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { UseSignal, ReactWidget } from './vdom';
  4. import { Kernel } from '@jupyterlab/services';
  5. import { Button, DefaultIconReact } from '@jupyterlab/ui-components';
  6. import { IIterator, find, map, some } from '@phosphor/algorithm';
  7. import { CommandRegistry } from '@phosphor/commands';
  8. import { Message, MessageLoop } from '@phosphor/messaging';
  9. import { AttachedProperty } from '@phosphor/properties';
  10. import { PanelLayout, Widget } from '@phosphor/widgets';
  11. import { IClientSession } from './clientsession';
  12. import * as React from 'react';
  13. import { ReadonlyJSONObject } from '@phosphor/coreutils';
  14. /**
  15. * The class name added to toolbars.
  16. */
  17. const TOOLBAR_CLASS = 'jp-Toolbar';
  18. /**
  19. * The class name added to toolbar items.
  20. */
  21. const TOOLBAR_ITEM_CLASS = 'jp-Toolbar-item';
  22. /**
  23. * The class name added to toolbar kernel name text.
  24. */
  25. const TOOLBAR_KERNEL_NAME_CLASS = 'jp-Toolbar-kernelName';
  26. /**
  27. * The class name added to toolbar spacer.
  28. */
  29. const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
  30. /**
  31. * The class name added to toolbar kernel status icon.
  32. */
  33. const TOOLBAR_KERNEL_STATUS_CLASS = 'jp-Toolbar-kernelStatus';
  34. /**
  35. * The class name added to a busy kernel indicator.
  36. */
  37. const TOOLBAR_BUSY_CLASS = 'jp-FilledCircleIcon';
  38. const TOOLBAR_IDLE_CLASS = 'jp-CircleIcon';
  39. /**
  40. * A layout for toolbars.
  41. *
  42. * #### Notes
  43. * This layout automatically collapses its height if there are no visible
  44. * toolbar widgets, and expands to the standard toolbar height if there are
  45. * visible toolbar widgets.
  46. */
  47. class ToolbarLayout extends PanelLayout {
  48. /**
  49. * A message handler invoked on a `'fit-request'` message.
  50. *
  51. * If any child widget is visible, expand the toolbar height to the normal
  52. * toolbar height.
  53. */
  54. protected onFitRequest(msg: Message): void {
  55. super.onFitRequest(msg);
  56. if (this.parent!.isAttached) {
  57. // If there are any widgets not explicitly hidden, expand the toolbar to
  58. // accommodate them.
  59. if (some(this.widgets, w => !w.isHidden)) {
  60. this.parent!.node.style.minHeight = 'var(--jp-private-toolbar-height)';
  61. } else {
  62. this.parent!.node.style.minHeight = '';
  63. }
  64. }
  65. // Set the dirty flag to ensure only a single update occurs.
  66. this._dirty = true;
  67. // Notify the ancestor that it should fit immediately. This may
  68. // cause a resize of the parent, fulfilling the required update.
  69. if (this.parent!.parent) {
  70. MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
  71. }
  72. // If the dirty flag is still set, the parent was not resized.
  73. // Trigger the required update on the parent widget immediately.
  74. if (this._dirty) {
  75. MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
  76. }
  77. }
  78. /**
  79. * A message handler invoked on an `'update-request'` message.
  80. */
  81. protected onUpdateRequest(msg: Message): void {
  82. super.onUpdateRequest(msg);
  83. if (this.parent!.isVisible) {
  84. this._dirty = false;
  85. }
  86. }
  87. /**
  88. * A message handler invoked on a `'child-shown'` message.
  89. */
  90. protected onChildShown(msg: Widget.ChildMessage): void {
  91. super.onChildShown(msg);
  92. // Post a fit request for the parent widget.
  93. this.parent!.fit();
  94. }
  95. /**
  96. * A message handler invoked on a `'child-hidden'` message.
  97. */
  98. protected onChildHidden(msg: Widget.ChildMessage): void {
  99. super.onChildHidden(msg);
  100. // Post a fit request for the parent widget.
  101. this.parent!.fit();
  102. }
  103. /**
  104. * A message handler invoked on a `'before-attach'` message.
  105. */
  106. protected onBeforeAttach(msg: Message): void {
  107. super.onBeforeAttach(msg);
  108. // Post a fit request for the parent widget.
  109. this.parent!.fit();
  110. }
  111. /**
  112. * Attach a widget to the parent's DOM node.
  113. *
  114. * @param index - The current index of the widget in the layout.
  115. *
  116. * @param widget - The widget to attach to the parent.
  117. *
  118. * #### Notes
  119. * This is a reimplementation of the superclass method.
  120. */
  121. protected attachWidget(index: number, widget: Widget): void {
  122. super.attachWidget(index, widget);
  123. // Post a fit request for the parent widget.
  124. this.parent!.fit();
  125. }
  126. /**
  127. * Detach a widget from the parent's DOM node.
  128. *
  129. * @param index - The previous index of the widget in the layout.
  130. *
  131. * @param widget - The widget to detach from the parent.
  132. *
  133. * #### Notes
  134. * This is a reimplementation of the superclass method.
  135. */
  136. protected detachWidget(index: number, widget: Widget): void {
  137. super.detachWidget(index, widget);
  138. // Post a fit request for the parent widget.
  139. this.parent!.fit();
  140. }
  141. private _dirty = false;
  142. }
  143. /**
  144. * A class which provides a toolbar widget.
  145. */
  146. export class Toolbar<T extends Widget = Widget> extends Widget {
  147. /**
  148. * Construct a new toolbar widget.
  149. */
  150. constructor() {
  151. super();
  152. this.addClass(TOOLBAR_CLASS);
  153. this.layout = new ToolbarLayout();
  154. }
  155. /**
  156. * Get an iterator over the ordered toolbar item names.
  157. *
  158. * @returns An iterator over the toolbar item names.
  159. */
  160. names(): IIterator<string> {
  161. let layout = this.layout as ToolbarLayout;
  162. return map(layout.widgets, widget => {
  163. return Private.nameProperty.get(widget);
  164. });
  165. }
  166. /**
  167. * Add an item to the end of the toolbar.
  168. *
  169. * @param name - The name of the widget to add to the toolbar.
  170. *
  171. * @param widget - The widget to add to the toolbar.
  172. *
  173. * @param index - The optional name of the item to insert after.
  174. *
  175. * @returns Whether the item was added to toolbar. Returns false if
  176. * an item of the same name is already in the toolbar.
  177. *
  178. * #### Notes
  179. * The item can be removed from the toolbar by setting its parent to `null`.
  180. */
  181. addItem(name: string, widget: T): boolean {
  182. let layout = this.layout as ToolbarLayout;
  183. return this.insertItem(layout.widgets.length, name, widget);
  184. }
  185. /**
  186. * Insert an item into the toolbar at the specified index.
  187. *
  188. * @param index - The index at which to insert the item.
  189. *
  190. * @param name - The name of the item.
  191. *
  192. * @param widget - The widget to add.
  193. *
  194. * @returns Whether the item was added to the toolbar. Returns false if
  195. * an item of the same name is already in the toolbar.
  196. *
  197. * #### Notes
  198. * The index will be clamped to the bounds of the items.
  199. * The item can be removed from the toolbar by setting its parent to `null`.
  200. */
  201. insertItem(index: number, name: string, widget: T): boolean {
  202. let existing = find(this.names(), value => value === name);
  203. if (existing) {
  204. return false;
  205. }
  206. widget.addClass(TOOLBAR_ITEM_CLASS);
  207. let layout = this.layout as ToolbarLayout;
  208. layout.insertWidget(index, widget);
  209. Private.nameProperty.set(widget, name);
  210. return true;
  211. }
  212. /**
  213. * Insert an item into the toolbar at the after a target item.
  214. *
  215. * @param at - The target item to insert after.
  216. *
  217. * @param name - The name of the item.
  218. *
  219. * @param widget - The widget to add.
  220. *
  221. * @returns Whether the item was added to the toolbar. Returns false if
  222. * an item of the same name is already in the toolbar.
  223. *
  224. * #### Notes
  225. * The index will be clamped to the bounds of the items.
  226. * The item can be removed from the toolbar by setting its parent to `null`.
  227. */
  228. insertAfter(at: string, name: string, widget: T): boolean {
  229. return this._insertRelative(at, 1, name, widget);
  230. }
  231. /**
  232. * Insert an item into the toolbar at the before a target item.
  233. *
  234. * @param at - The target item to insert before.
  235. *
  236. * @param name - The name of the item.
  237. *
  238. * @param widget - The widget to add.
  239. *
  240. * @returns Whether the item was added to the toolbar. Returns false if
  241. * an item of the same name is already in the toolbar.
  242. *
  243. * #### Notes
  244. * The index will be clamped to the bounds of the items.
  245. * The item can be removed from the toolbar by setting its parent to `null`.
  246. */
  247. insertBefore(at: string, name: string, widget: T): boolean {
  248. return this._insertRelative(at, 0, name, widget);
  249. }
  250. private _insertRelative(
  251. at: string,
  252. offset: number,
  253. name: string,
  254. widget: T
  255. ): boolean {
  256. let nameWithIndex = map(this.names(), (name, i) => {
  257. return { name: name, index: i };
  258. });
  259. let target = find(nameWithIndex, x => x.name === at);
  260. if (target) {
  261. return this.insertItem(target.index + offset, name, widget);
  262. }
  263. return false;
  264. }
  265. /**
  266. * Handle the DOM events for the widget.
  267. *
  268. * @param event - The DOM event sent to the widget.
  269. *
  270. * #### Notes
  271. * This method implements the DOM `EventListener` interface and is
  272. * called in response to events on the dock panel's node. It should
  273. * not be called directly by user code.
  274. */
  275. handleEvent(event: Event): void {
  276. switch (event.type) {
  277. case 'click':
  278. this.handleClick(event);
  279. break;
  280. default:
  281. break;
  282. }
  283. }
  284. /**
  285. * Handle a DOM click event.
  286. */
  287. protected handleClick(event: Event) {
  288. // Clicking a label focuses the corresponding control, so let it be.
  289. if (event.target instanceof HTMLLabelElement) {
  290. return;
  291. }
  292. // If this click already focused a control, let it be.
  293. if (this.node.contains(document.activeElement)) {
  294. return;
  295. }
  296. // Otherwise, activate the parent widget, which may take focus if desired.
  297. if (this.parent) {
  298. this.parent.activate();
  299. }
  300. }
  301. /**
  302. * Handle `after-attach` messages for the widget.
  303. */
  304. protected onAfterAttach(msg: Message): void {
  305. this.node.addEventListener('click', this);
  306. }
  307. /**
  308. * Handle `before-detach` messages for the widget.
  309. */
  310. protected onBeforeDetach(msg: Message): void {
  311. this.node.removeEventListener('click', this);
  312. }
  313. }
  314. /**
  315. * The namespace for Toolbar class statics.
  316. */
  317. export namespace Toolbar {
  318. /**
  319. * Create an interrupt toolbar item.
  320. */
  321. export function createInterruptButton(session: IClientSession): Widget {
  322. return new ToolbarButton({
  323. iconClassName: 'jp-StopIcon',
  324. onClick: () => {
  325. if (session.kernel) {
  326. void session.kernel.interrupt();
  327. }
  328. },
  329. tooltip: 'Interrupt the kernel'
  330. });
  331. }
  332. /**
  333. * Create a restart toolbar item.
  334. */
  335. export function createRestartButton(session: IClientSession): Widget {
  336. return new ToolbarButton({
  337. iconClassName: 'jp-RefreshIcon',
  338. onClick: () => {
  339. void session.restart();
  340. },
  341. tooltip: 'Restart the kernel'
  342. });
  343. }
  344. /**
  345. * Create a toolbar spacer item.
  346. *
  347. * #### Notes
  348. * It is a flex spacer that separates the left toolbar items
  349. * from the right toolbar items.
  350. */
  351. export function createSpacerItem(): Widget {
  352. return new Private.Spacer();
  353. }
  354. /**
  355. * Create a kernel name indicator item.
  356. *
  357. * #### Notes
  358. * It will display the `'display_name`' of the current kernel,
  359. * or `'No Kernel!'` if there is no kernel.
  360. * It can handle a change in context or kernel.
  361. */
  362. export function createKernelNameItem(session: IClientSession): Widget {
  363. const el = ReactWidget.create(
  364. <Private.KernelNameComponent session={session} />
  365. );
  366. el.addClass('jp-KernelName');
  367. return el;
  368. }
  369. /**
  370. * Create a kernel status indicator item.
  371. *
  372. * #### Notes
  373. * It will show a busy status if the kernel status is busy.
  374. * It will show the current status in the node title.
  375. * It can handle a change to the context or the kernel.
  376. */
  377. export function createKernelStatusItem(session: IClientSession): Widget {
  378. return new Private.KernelStatus(session);
  379. }
  380. }
  381. /**
  382. * Namespace for ToolbarButtonComponent.
  383. */
  384. export namespace ToolbarButtonComponent {
  385. /**
  386. * Interface for ToolbarButttonComponent props.
  387. */
  388. export interface IProps {
  389. className?: string;
  390. label?: string;
  391. iconClassName?: string;
  392. iconLabel?: string;
  393. tooltip?: string;
  394. onClick?: () => void;
  395. enabled?: boolean;
  396. }
  397. }
  398. /**
  399. * React component for a toolbar button.
  400. *
  401. * @param props - The props for ToolbarButtonComponent.
  402. */
  403. export function ToolbarButtonComponent(props: ToolbarButtonComponent.IProps) {
  404. // In some browsers, a button click event moves the focus from the main
  405. // content to the button (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus).
  406. // We avoid a click event by calling preventDefault in mousedown, and
  407. // we bind the button action to `mousedown`.
  408. const handleMouseDown = (event: React.MouseEvent) => {
  409. // Fire action only when left button is pressed.
  410. if (event.button === 0) {
  411. event.preventDefault();
  412. props.onClick();
  413. }
  414. };
  415. const handleKeyDown = (event: React.KeyboardEvent) => {
  416. const { key } = event;
  417. if (key === 'Enter' || key === ' ') {
  418. props.onClick();
  419. }
  420. };
  421. return (
  422. <Button
  423. className={
  424. props.className
  425. ? props.className + ' jp-ToolbarButtonComponent'
  426. : 'jp-ToolbarButtonComponent'
  427. }
  428. disabled={props.enabled === false}
  429. onMouseDown={handleMouseDown}
  430. onKeyDown={handleKeyDown}
  431. title={props.tooltip || props.iconLabel}
  432. minimal
  433. >
  434. {props.iconClassName && (
  435. <DefaultIconReact
  436. name={`${props.iconClassName} jp-Icon jp-Icon-16`}
  437. className={'jp-ToolbarButtonComponent-icon'}
  438. fallback={true}
  439. center={true}
  440. kind={'toolbarButton'}
  441. tag={'span'}
  442. />
  443. )}
  444. {props.label && (
  445. <span className="jp-ToolbarButtonComponent-label">{props.label}</span>
  446. )}
  447. </Button>
  448. );
  449. }
  450. /**
  451. * Adds the toolbar button class to the toolbar widget.
  452. * @param w Toolbar button widget.
  453. */
  454. export function addToolbarButtonClass(w: Widget): Widget {
  455. w.addClass('jp-ToolbarButton');
  456. return w;
  457. }
  458. /**
  459. * Phosphor Widget version of static ToolbarButtonComponent.
  460. */
  461. export class ToolbarButton extends ReactWidget {
  462. /**
  463. * Creates a toolbar button
  464. * @param props props for underlying `ToolbarButton` componenent
  465. */
  466. constructor(private props: ToolbarButtonComponent.IProps = {}) {
  467. super();
  468. addToolbarButtonClass(this);
  469. }
  470. render() {
  471. return <ToolbarButtonComponent {...this.props} />;
  472. }
  473. }
  474. /**
  475. * Namespace for CommandToolbarButtonComponent.
  476. */
  477. export namespace CommandToolbarButtonComponent {
  478. /**
  479. * Interface for CommandToolbarButtonComponent props.
  480. */
  481. export interface IProps {
  482. commands: CommandRegistry;
  483. id: string;
  484. args?: ReadonlyJSONObject;
  485. }
  486. }
  487. /**
  488. * React component for a toolbar button that wraps a command.
  489. *
  490. * This wraps the ToolbarButtonComponent and watches the command registry
  491. * for changes to the command.
  492. */
  493. export function CommandToolbarButtonComponent(
  494. props: CommandToolbarButtonComponent.IProps
  495. ) {
  496. return (
  497. <UseSignal
  498. signal={props.commands.commandChanged}
  499. shouldUpdate={(sender, args) =>
  500. (args.id === props.id && args.type === 'changed') ||
  501. args.type === 'many-changed'
  502. }
  503. >
  504. {() => <ToolbarButtonComponent {...Private.propsFromCommand(props)} />}
  505. </UseSignal>
  506. );
  507. }
  508. /*
  509. * Adds the command toolbar button class to the command toolbar widget.
  510. * @param w Command toolbar button widget.
  511. */
  512. export function addCommandToolbarButtonClass(w: Widget): Widget {
  513. w.addClass('jp-CommandToolbarButton');
  514. return w;
  515. }
  516. /**
  517. * Phosphor Widget version of CommandToolbarButtonComponent.
  518. */
  519. export class CommandToolbarButton extends ReactWidget {
  520. /**
  521. * Creates a command toolbar button
  522. * @param props props for underlying `CommandToolbarButtonComponent` componenent
  523. */
  524. constructor(private props: CommandToolbarButtonComponent.IProps) {
  525. super();
  526. addCommandToolbarButtonClass(this);
  527. }
  528. render() {
  529. return <CommandToolbarButtonComponent {...this.props} />;
  530. }
  531. }
  532. /**
  533. * A namespace for private data.
  534. */
  535. namespace Private {
  536. export function propsFromCommand(
  537. options: CommandToolbarButtonComponent.IProps
  538. ): ToolbarButtonComponent.IProps {
  539. let { commands, id, args } = options;
  540. const iconClassName = commands.iconClass(id, args);
  541. const iconLabel = commands.iconLabel(id, args);
  542. const label = commands.label(id, args);
  543. let className = commands.className(id, args);
  544. // Add the boolean state classes.
  545. if (commands.isToggled(id, args)) {
  546. className += ' p-mod-toggled';
  547. }
  548. if (!commands.isVisible(id, args)) {
  549. className += ' p-mod-hidden';
  550. }
  551. const tooltip = commands.caption(id, args) || label || iconLabel;
  552. const onClick = () => {
  553. void commands.execute(id, args);
  554. };
  555. const enabled = commands.isEnabled(id, args);
  556. return { className, iconClassName, tooltip, onClick, enabled, label };
  557. }
  558. /**
  559. * An attached property for the name of a toolbar item.
  560. */
  561. export const nameProperty = new AttachedProperty<Widget, string>({
  562. name: 'name',
  563. create: () => ''
  564. });
  565. /**
  566. * A no-op function.
  567. */
  568. export function noOp() {
  569. /* no-op */
  570. }
  571. /**
  572. * A spacer widget.
  573. */
  574. export class Spacer extends Widget {
  575. /**
  576. * Construct a new spacer widget.
  577. */
  578. constructor() {
  579. super();
  580. this.addClass(TOOLBAR_SPACER_CLASS);
  581. }
  582. }
  583. /**
  584. * Namespace for KernelNameComponent.
  585. */
  586. export namespace KernelNameComponent {
  587. /**
  588. * Interface for KernelNameComponent props.
  589. */
  590. export interface IProps {
  591. session: IClientSession;
  592. }
  593. }
  594. /**
  595. * React component for a kernel name button.
  596. *
  597. * This wraps the ToolbarButtonComponent and watches the kernel
  598. * session for changes.
  599. */
  600. export function KernelNameComponent(props: KernelNameComponent.IProps) {
  601. return (
  602. <UseSignal
  603. signal={props.session.kernelChanged}
  604. initialSender={props.session}
  605. >
  606. {session => (
  607. <ToolbarButtonComponent
  608. className={TOOLBAR_KERNEL_NAME_CLASS}
  609. onClick={props.session.selectKernel.bind(props.session)}
  610. tooltip={'Switch kernel'}
  611. label={session.kernelDisplayName}
  612. />
  613. )}
  614. </UseSignal>
  615. );
  616. }
  617. /**
  618. * A toolbar item that displays kernel status.
  619. */
  620. export class KernelStatus extends Widget {
  621. /**
  622. * Construct a new kernel status widget.
  623. */
  624. constructor(session: IClientSession) {
  625. super();
  626. this.addClass(TOOLBAR_KERNEL_STATUS_CLASS);
  627. this._onStatusChanged(session);
  628. session.statusChanged.connect(this._onStatusChanged, this);
  629. }
  630. /**
  631. * Handle a status on a kernel.
  632. */
  633. private _onStatusChanged(session: IClientSession) {
  634. if (this.isDisposed) {
  635. return;
  636. }
  637. let status = session.kernel.status;
  638. const busy = this._isBusy(status);
  639. this.toggleClass(TOOLBAR_BUSY_CLASS, busy);
  640. this.toggleClass(TOOLBAR_IDLE_CLASS, !busy);
  641. let title = 'Kernel ' + status[0].toUpperCase() + status.slice(1);
  642. this.node.title = title;
  643. }
  644. /**
  645. * Check if status should be shown as busy.
  646. */
  647. private _isBusy(status: Kernel.Status): boolean {
  648. return (
  649. status === 'busy' || status === 'starting' || status === 'restarting'
  650. );
  651. }
  652. }
  653. }