toolbar.tsx 22 KB

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