widget.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Kernel, KernelMessage
  5. } from '@jupyterlab/services';
  6. import {
  7. defineSignal, ISignal
  8. } from 'phosphor/lib/core/signaling';
  9. import {
  10. Message
  11. } from 'phosphor/lib/core/messaging';
  12. import {
  13. PanelLayout
  14. } from 'phosphor/lib/ui/panel';
  15. import {
  16. Widget
  17. } from 'phosphor/lib/ui/widget';
  18. import {
  19. IChangedArgs
  20. } from '../../common/interfaces';
  21. import {
  22. CodeEditor
  23. } from '../../codeeditor';
  24. import {
  25. RenderMime
  26. } from '../../rendermime';
  27. import {
  28. IMetadataCursor
  29. } from '../common/metadata';
  30. import {
  31. OutputAreaWidget
  32. } from '../output-area';
  33. import {
  34. ICellEditorWidget, CodeCellEditorWidget
  35. } from './editor';
  36. import {
  37. ICellModel, ICodeCellModel,
  38. IMarkdownCellModel, IRawCellModel
  39. } from './model';
  40. /**
  41. * The class name added to cell widgets.
  42. */
  43. const CELL_CLASS = 'jp-Cell';
  44. /**
  45. * The class name added to the prompt area of cell.
  46. */
  47. const PROMPT_CLASS = 'jp-Cell-prompt';
  48. /**
  49. * The class name added to input area widgets.
  50. */
  51. const INPUT_CLASS = 'jp-InputArea';
  52. /**
  53. * The class name added to the editor area of the cell.
  54. */
  55. const EDITOR_CLASS = 'jp-InputArea-editor';
  56. /**
  57. * The class name added to the cell when collapsed.
  58. */
  59. const COLLAPSED_CLASS = 'jp-mod-collapsed';
  60. /**
  61. * The class name added to the cell when readonly.
  62. */
  63. const READONLY_CLASS = 'jp-mod-readOnly';
  64. /**
  65. * The class name added to code cells.
  66. */
  67. const CODE_CELL_CLASS = 'jp-CodeCell';
  68. /**
  69. * The class name added to markdown cells.
  70. */
  71. const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell';
  72. /**
  73. * The class name added to rendered markdown output widgets.
  74. */
  75. const MARKDOWN_OUTPUT_CLASS = 'jp-MarkdownOutput';
  76. /**
  77. * The class name added to raw cells.
  78. */
  79. const RAW_CELL_CLASS = 'jp-RawCell';
  80. /**
  81. * The class name added to a rendered input area.
  82. */
  83. const RENDERED_CLASS = 'jp-mod-rendered';
  84. /**
  85. * The text applied to an empty markdown cell.
  86. */
  87. const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
  88. /**
  89. * A base cell widget.
  90. */
  91. export
  92. class BaseCellWidget extends Widget {
  93. /**
  94. * Construct a new base cell widget.
  95. */
  96. constructor(options: BaseCellWidget.IOptions) {
  97. super();
  98. this.addClass(CELL_CLASS);
  99. this.layout = new PanelLayout();
  100. let renderer = options.renderer;
  101. this._editor = renderer.createCellEditor();
  102. this._input = renderer.createInputArea(this._editor);
  103. (this.layout as PanelLayout).addWidget(this._input);
  104. }
  105. /**
  106. * A signal emitted when the model of the cell changes.
  107. */
  108. modelChanged: ISignal<BaseCellWidget, void>;
  109. /**
  110. * The model used by the widget.
  111. */
  112. get model(): ICellModel {
  113. return this._model;
  114. }
  115. set model(newValue: ICellModel) {
  116. if (!newValue && !this._model || newValue === this._model) {
  117. return;
  118. }
  119. let oldValue: ICellModel = this._model;
  120. this._model = newValue;
  121. // Trigger private, protected, and public updates.
  122. this._onModelChanged(oldValue, newValue);
  123. this.onModelChanged(oldValue, newValue);
  124. this.modelChanged.emit(void 0);
  125. }
  126. /**
  127. * Get the prompt node used by the cell.
  128. */
  129. get promptNode(): HTMLElement {
  130. return this._input.promptNode;
  131. }
  132. /**
  133. * Get the editor widget used by the cell.
  134. *
  135. * #### Notes
  136. * This is a ready-only property.
  137. */
  138. get editor(): ICellEditorWidget {
  139. return this._editor;
  140. }
  141. /**
  142. * The mimetype used by the cell.
  143. */
  144. get mimetype(): string {
  145. return this._mimetype;
  146. }
  147. set mimetype(value: string) {
  148. if (!value) {
  149. return;
  150. }
  151. if (this._mimetype === value) {
  152. return;
  153. }
  154. this._mimetype = value;
  155. this.editor.setMimeType(value);
  156. }
  157. /**
  158. * The read only state of the cell.
  159. */
  160. get readOnly(): boolean {
  161. return this._readOnly;
  162. }
  163. set readOnly(value: boolean) {
  164. if (value === this._readOnly) {
  165. return;
  166. }
  167. this._readOnly = value;
  168. this.update();
  169. }
  170. /**
  171. * The trusted state of the cell.
  172. */
  173. get trusted(): boolean {
  174. return this._trusted;
  175. }
  176. set trusted(value: boolean) {
  177. if (!this._model) {
  178. return;
  179. }
  180. this._trustedCursor.setValue(value);
  181. this._trusted = value;
  182. }
  183. /**
  184. * Set the prompt for the widget.
  185. */
  186. setPrompt(value: string): void {
  187. this._input.setPrompt(value);
  188. }
  189. /**
  190. * Dispose of the resources held by the widget.
  191. */
  192. dispose() {
  193. // Do nothing if already disposed.
  194. if (this.isDisposed) {
  195. return;
  196. }
  197. this._model = null;
  198. this._input = null;
  199. this._editor = null;
  200. this._trustedCursor = null;
  201. super.dispose();
  202. }
  203. /**
  204. * Handle `after-attach` messages.
  205. */
  206. protected onAfterAttach(msg: Message): void {
  207. this.update();
  208. }
  209. /**
  210. * Handle `'activate-request'` messages.
  211. */
  212. protected onActivateRequest(msg: Message): void {
  213. this._editor.activate();
  214. }
  215. /**
  216. * Handle `update-request` messages.
  217. */
  218. protected onUpdateRequest(msg: Message): void {
  219. if (!this._model) {
  220. return;
  221. }
  222. // Handle read only state.
  223. this._editor.setReadOnly(this._readOnly);
  224. this.toggleClass(READONLY_CLASS, this._readOnly);
  225. }
  226. /**
  227. * Handle changes in the model.
  228. *
  229. * #### Notes
  230. * Subclasses may reimplement this method as needed.
  231. */
  232. protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
  233. // no-op
  234. }
  235. /**
  236. * Handle changes in the model.
  237. */
  238. protected onMetadataChanged(model: ICellModel, args: IChangedArgs<any>): void {
  239. switch (args.name) {
  240. case 'trusted':
  241. this._trusted = !!this._trustedCursor.getValue();
  242. this.update();
  243. break;
  244. default:
  245. break;
  246. }
  247. }
  248. /**
  249. * Handle a new model.
  250. *
  251. * #### Notes
  252. * This method is called after the model change has been handled
  253. * internally and before the `modelChanged` signal is emitted.
  254. * The default implementation is a no-op.
  255. */
  256. protected onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
  257. // no-op
  258. }
  259. /**
  260. * Render an input instead of the text editor.
  261. */
  262. protected renderInput(widget: Widget): void {
  263. this.addClass(RENDERED_CLASS);
  264. this._input.renderInput(widget);
  265. }
  266. /**
  267. * Show the text editor.
  268. */
  269. protected showEditor(): void {
  270. this.removeClass(RENDERED_CLASS);
  271. this._input.showEditor();
  272. }
  273. /**
  274. * Handle a model change.
  275. */
  276. private _onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
  277. // If the model is being replaced, disconnect the old signal handler.
  278. if (oldValue) {
  279. oldValue.stateChanged.disconnect(this.onModelStateChanged, this);
  280. oldValue.metadataChanged.disconnect(this.onMetadataChanged, this);
  281. }
  282. // Reset the editor model and set its mode to be the default MIME type.
  283. this._editor.model = this._model;
  284. this._editor.setMimeType(this._mimetype);
  285. // Handle trusted cursor.
  286. this._trustedCursor = this._model.getMetadata('trusted');
  287. this._trusted = !!this._trustedCursor.getValue();
  288. // Connect signal handlers.
  289. this._model.metadataChanged.connect(this.onMetadataChanged, this);
  290. this._model.stateChanged.connect(this.onModelStateChanged, this);
  291. }
  292. private _input: InputAreaWidget = null;
  293. private _editor: ICellEditorWidget = null;
  294. private _model: ICellModel = null;
  295. private _mimetype = 'text/plain';
  296. private _readOnly = false;
  297. private _trustedCursor: IMetadataCursor = null;
  298. private _trusted = false;
  299. }
  300. // Define the signals for the `BaseCellWidget` class.
  301. defineSignal(BaseCellWidget.prototype, 'modelChanged');
  302. /**
  303. * The namespace for the `BaseCellWidget` class statics.
  304. */
  305. export
  306. namespace BaseCellWidget {
  307. /**
  308. * An options object for initializing a base cell widget.
  309. */
  310. export
  311. interface IOptions {
  312. /**
  313. * A renderer for creating cell widgets.
  314. *
  315. * The default is a shared renderer instance.
  316. */
  317. renderer: IRenderer;
  318. }
  319. /**
  320. * A renderer for creating cell widgets.
  321. */
  322. export
  323. interface IRenderer {
  324. /**
  325. * Create a new cell editor for the widget.
  326. */
  327. createCellEditor(): ICellEditorWidget;
  328. /**
  329. * Create a new input area for the widget.
  330. */
  331. createInputArea(editor: ICellEditorWidget): InputAreaWidget;
  332. }
  333. /**
  334. * The default implementation of an `IRenderer`.
  335. */
  336. export
  337. class Renderer implements IRenderer {
  338. /**
  339. * A code editor factory.
  340. */
  341. readonly editorFactory: (host: Widget) => CodeEditor.IEditor;
  342. /**
  343. * Creates a new renderer.
  344. */
  345. constructor(options: Renderer.IOptions) {
  346. this.editorFactory = options.editorFactory;
  347. }
  348. /**
  349. * Create a new cell editor for the widget.
  350. */
  351. createCellEditor(): ICellEditorWidget {
  352. return new CodeCellEditorWidget(this.editorFactory);
  353. }
  354. /**
  355. * Create a new input area for the widget.
  356. */
  357. createInputArea(editor: ICellEditorWidget): InputAreaWidget {
  358. return new InputAreaWidget(editor);
  359. }
  360. }
  361. /**
  362. * The namespace for the `Renderer` class statics.
  363. */
  364. export
  365. namespace Renderer {
  366. /**
  367. * An options object for initializing a renderer.
  368. */
  369. export
  370. interface IOptions {
  371. /**
  372. * A code editor factory.
  373. */
  374. readonly editorFactory: (host: Widget) => CodeEditor.IEditor;
  375. }
  376. }
  377. }
  378. /**
  379. * A widget for a code cell.
  380. */
  381. export
  382. class CodeCellWidget extends BaseCellWidget {
  383. /**
  384. * Construct a code cell widget.
  385. */
  386. constructor(options: CodeCellWidget.IOptions) {
  387. super(options);
  388. this.addClass(CODE_CELL_CLASS);
  389. this._rendermime = options.rendermime;
  390. this._renderer = options.renderer;
  391. }
  392. /**
  393. * The model used by the widget.
  394. */
  395. model: ICodeCellModel;
  396. /**
  397. * Dispose of the resources used by the widget.
  398. */
  399. dispose(): void {
  400. if (this.isDisposed) {
  401. return;
  402. }
  403. this._collapsedCursor = null;
  404. this._scrolledCursor = null;
  405. this._output = null;
  406. super.dispose();
  407. }
  408. /**
  409. * Execute the cell given a kernel.
  410. */
  411. execute(kernel: Kernel.IKernel): Promise<KernelMessage.IExecuteReplyMsg> {
  412. let model = this.model;
  413. let code = model.source;
  414. if (!code.trim()) {
  415. model.executionCount = null;
  416. model.outputs.clear();
  417. return Promise.resolve(null);
  418. }
  419. model.executionCount = null;
  420. this.setPrompt('*');
  421. this.trusted = true;
  422. let outputs = model.outputs;
  423. return outputs.execute(code, kernel).then(reply => {
  424. let status = reply.content.status;
  425. if (status === 'abort') {
  426. model.executionCount = null;
  427. this.setPrompt(' ');
  428. } else {
  429. model.executionCount = reply.content.execution_count;
  430. }
  431. return reply;
  432. });
  433. }
  434. /**
  435. * Handle `update-request` messages.
  436. */
  437. protected onUpdateRequest(msg: Message): void {
  438. if (this._collapsedCursor) {
  439. let value = this._collapsedCursor.getValue() as boolean;
  440. this.toggleClass(COLLAPSED_CLASS, value);
  441. }
  442. if (this._output) {
  443. // TODO: handle scrolled state.
  444. this._output.trusted = this.trusted;
  445. }
  446. super.onUpdateRequest(msg);
  447. }
  448. /**
  449. * Handle the widget receiving a new model.
  450. */
  451. protected onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
  452. let model = newValue as ICodeCellModel;
  453. let renderer = this._renderer;
  454. if (!this._output) {
  455. this._output = renderer.createOutputArea(this._rendermime);
  456. (this.layout as PanelLayout).addWidget(this._output);
  457. }
  458. this._output.model = model.outputs;
  459. this._output.trusted = this.trusted;
  460. this._collapsedCursor = model.getMetadata('collapsed');
  461. this._scrolledCursor = model.getMetadata('scrolled');
  462. this.setPrompt(`${model.executionCount || ''}`);
  463. }
  464. /**
  465. * Handle changes in the model.
  466. */
  467. protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
  468. switch (args.name) {
  469. case 'executionCount':
  470. this.setPrompt(`${(model as ICodeCellModel).executionCount}`);
  471. break;
  472. default:
  473. break;
  474. }
  475. super.onModelStateChanged(model, args);
  476. }
  477. /**
  478. * Handle changes in the metadata.
  479. */
  480. protected onMetadataChanged(model: ICellModel, args: IChangedArgs<any>): void {
  481. switch (args.name) {
  482. case 'collapsed':
  483. case 'scrolled':
  484. this.update();
  485. break;
  486. default:
  487. break;
  488. }
  489. super.onMetadataChanged(model, args);
  490. }
  491. private _renderer: CodeCellWidget.IRenderer;
  492. private _rendermime: RenderMime = null;
  493. private _output: OutputAreaWidget = null;
  494. private _collapsedCursor: IMetadataCursor = null;
  495. private _scrolledCursor: IMetadataCursor = null;
  496. }
  497. /**
  498. * The namespace for the `CodeCellWidget` class statics.
  499. */
  500. export
  501. namespace CodeCellWidget {
  502. /**
  503. * An options object for initializing a base cell widget.
  504. */
  505. export
  506. interface IOptions {
  507. /**
  508. * A renderer for creating cell widgets.
  509. *
  510. * The default is a shared renderer instance.
  511. */
  512. renderer: IRenderer;
  513. /**
  514. * The mime renderer for the cell widget.
  515. */
  516. rendermime: RenderMime;
  517. }
  518. /**
  519. * A renderer for creating code cell widgets.
  520. */
  521. export
  522. interface IRenderer extends BaseCellWidget.IRenderer {
  523. /**
  524. * Create a new output area for the widget.
  525. */
  526. createOutputArea(rendermime: RenderMime): OutputAreaWidget;
  527. }
  528. /**
  529. * The default implementation of an `IRenderer`.
  530. */
  531. export
  532. class Renderer extends BaseCellWidget.Renderer implements IRenderer {
  533. /**
  534. * Create an output area widget.
  535. */
  536. createOutputArea(rendermime: RenderMime): OutputAreaWidget {
  537. return new OutputAreaWidget({ rendermime });
  538. }
  539. }
  540. }
  541. /**
  542. * A widget for a Markdown cell.
  543. *
  544. * #### Notes
  545. * Things get complicated if we want the rendered text to update
  546. * any time the text changes, the text editor model changes,
  547. * or the input area model changes. We don't support automatically
  548. * updating the rendered text in all of these cases.
  549. */
  550. export
  551. class MarkdownCellWidget extends BaseCellWidget {
  552. /**
  553. * Construct a Markdown cell widget.
  554. */
  555. constructor(options: MarkdownCellWidget.IOptions) {
  556. super(options);
  557. this.addClass(MARKDOWN_CELL_CLASS);
  558. // Insist on the Github-flavored markdown mode.
  559. this.mimetype = 'text/x-ipythongfm';
  560. this._rendermime = options.rendermime;
  561. }
  562. /**
  563. * The model used by the widget.
  564. */
  565. model: IMarkdownCellModel;
  566. /**
  567. * Whether the cell is rendered.
  568. */
  569. get rendered(): boolean {
  570. return this._rendered;
  571. }
  572. set rendered(value: boolean) {
  573. if (value === this._rendered) {
  574. return;
  575. }
  576. this._rendered = value;
  577. this.update();
  578. }
  579. /**
  580. * Dispose of the resource held by the widget.
  581. */
  582. dispose(): void {
  583. if (this.isDisposed) {
  584. return;
  585. }
  586. this._output = null;
  587. super.dispose();
  588. }
  589. /**
  590. * Handle `update-request` messages.
  591. */
  592. protected onUpdateRequest(msg: Message): void {
  593. let model = this.model;
  594. if (this.rendered) {
  595. let text = model && model.source || DEFAULT_MARKDOWN_TEXT;
  596. // Do not re-render if the text has not changed.
  597. if (text !== this._prev) {
  598. let bundle: RenderMime.MimeMap<string> = { 'text/markdown': text };
  599. let trusted = this.trusted;
  600. let widget = this._rendermime.render({ bundle, trusted });
  601. this._output = widget || new Widget();
  602. this._output.addClass(MARKDOWN_OUTPUT_CLASS);
  603. this.update();
  604. } else {
  605. this._output.show();
  606. }
  607. this._prev = text;
  608. this.renderInput(this._output);
  609. } else {
  610. this.showEditor();
  611. }
  612. super.onUpdateRequest(msg);
  613. }
  614. private _rendermime: RenderMime = null;
  615. private _output: Widget = null;
  616. private _rendered = true;
  617. private _prev = '';
  618. }
  619. /**
  620. * The namespace for the `CodeCellWidget` class statics.
  621. */
  622. export
  623. namespace MarkdownCellWidget {
  624. /**
  625. * An options object for initializing a base cell widget.
  626. */
  627. export
  628. interface IOptions {
  629. /**
  630. * A renderer for creating cell widgets.
  631. *
  632. * The default is a shared renderer instance.
  633. */
  634. renderer: BaseCellWidget.IRenderer;
  635. /**
  636. * The mime renderer for the cell widget.
  637. */
  638. rendermime: RenderMime;
  639. }
  640. }
  641. /**
  642. * A widget for a raw cell.
  643. */
  644. export
  645. class RawCellWidget extends BaseCellWidget {
  646. /**
  647. * Construct a raw cell widget.
  648. */
  649. constructor(options: BaseCellWidget.IOptions) {
  650. super(options);
  651. this.addClass(RAW_CELL_CLASS);
  652. }
  653. /**
  654. * The model used by the widget.
  655. */
  656. model: IRawCellModel;
  657. }
  658. /**
  659. * An input area widget, which hosts a prompt and an editor widget.
  660. */
  661. export
  662. class InputAreaWidget extends Widget {
  663. /**
  664. * Construct an input area widget.
  665. */
  666. constructor(editor: ICellEditorWidget) {
  667. super();
  668. this.addClass(INPUT_CLASS);
  669. editor.addClass(EDITOR_CLASS);
  670. this.layout = new PanelLayout();
  671. this._editor = editor;
  672. let prompt = this._prompt = new Widget();
  673. prompt.addClass(PROMPT_CLASS);
  674. let layout = this.layout as PanelLayout;
  675. layout.addWidget(prompt);
  676. layout.addWidget(editor);
  677. }
  678. /**
  679. * Get the prompt node used by the cell.
  680. */
  681. get promptNode(): HTMLElement {
  682. return this._prompt.node;
  683. }
  684. /**
  685. * Render an input instead of the text editor.
  686. */
  687. renderInput(widget: Widget): void {
  688. this._editor.hide();
  689. let layout = this.layout as PanelLayout;
  690. if (this._rendered) {
  691. layout.removeWidget(this._rendered);
  692. }
  693. this._rendered = widget;
  694. widget.show();
  695. layout.addWidget(widget);
  696. }
  697. /**
  698. * Show the text editor.
  699. */
  700. showEditor(): void {
  701. this._editor.show();
  702. let layout = this.layout as PanelLayout;
  703. if (this._rendered) {
  704. layout.removeWidget(this._rendered);
  705. }
  706. }
  707. /**
  708. * Set the prompt of the input area.
  709. */
  710. setPrompt(value: string): void {
  711. if (value === 'null') {
  712. value = ' ';
  713. }
  714. let text = `In [${value || ' '}]:`;
  715. this._prompt.node.textContent = text;
  716. }
  717. private _prompt: Widget;
  718. private _editor: ICellEditorWidget;
  719. private _rendered: Widget;
  720. }