UNPKG

15.8 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { Cell, ICellModel } from '@jupyterlab/cells';
5import { CodeEditor, JSONEditor } from '@jupyterlab/codeeditor';
6import { ObservableJSON } from '@jupyterlab/observables';
7import { IMapChange } from '@jupyter/ydoc';
8import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9import { Collapser } from '@jupyterlab/ui-components';
10import { ArrayExt } from '@lumino/algorithm';
11import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
12import { ConflatableMessage, Message, MessageLoop } from '@lumino/messaging';
13import { PanelLayout, Widget } from '@lumino/widgets';
14import { INotebookModel } from './model';
15import { NotebookPanel } from './panel';
16import { INotebookTools, INotebookTracker } from './tokens';
17
18class RankedPanel<T extends Widget = Widget> extends Widget {
19 constructor() {
20 super();
21 this.layout = new PanelLayout();
22 this.addClass('jp-RankedPanel');
23 }
24
25 addWidget(widget: Widget, rank: number): void {
26 const rankItem = { widget, rank };
27 const index = ArrayExt.upperBound(this._items, rankItem, Private.itemCmp);
28 ArrayExt.insert(this._items, index, rankItem);
29
30 const layout = this.layout as PanelLayout;
31 layout.insertWidget(index, widget);
32 }
33
34 /**
35 * Handle the removal of a child
36 *
37 */
38 protected onChildRemoved(msg: Widget.ChildMessage): void {
39 const index = ArrayExt.findFirstIndex(
40 this._items,
41 item => item.widget === msg.child
42 );
43 if (index !== -1) {
44 ArrayExt.removeAt(this._items, index);
45 }
46 }
47
48 private _items: Private.IRankItem<T>[] = [];
49}
50
51/**
52 * A widget that provides metadata tools.
53 */
54export class NotebookTools extends Widget implements INotebookTools {
55 /**
56 * Construct a new NotebookTools object.
57 */
58 constructor(options: NotebookTools.IOptions) {
59 super();
60 this.addClass('jp-NotebookTools');
61
62 this.translator = options.translator || nullTranslator;
63
64 this._tools = [];
65
66 this.layout = new PanelLayout();
67
68 this._tracker = options.tracker;
69 this._tracker.currentChanged.connect(
70 this._onActiveNotebookPanelChanged,
71 this
72 );
73 this._tracker.activeCellChanged.connect(this._onActiveCellChanged, this);
74 this._tracker.selectionChanged.connect(this._onSelectionChanged, this);
75 this._onActiveNotebookPanelChanged();
76 this._onActiveCellChanged();
77 this._onSelectionChanged();
78 }
79
80 /**
81 * The active cell widget.
82 */
83 get activeCell(): Cell | null {
84 return this._tracker.activeCell;
85 }
86
87 /**
88 * The currently selected cells.
89 */
90 get selectedCells(): Cell[] {
91 const panel = this._tracker.currentWidget;
92 if (!panel) {
93 return [];
94 }
95 const notebook = panel.content;
96 return notebook.widgets.filter(cell => notebook.isSelectedOrActive(cell));
97 }
98
99 /**
100 * The current notebook.
101 */
102 get activeNotebookPanel(): NotebookPanel | null {
103 return this._tracker.currentWidget;
104 }
105
106 /**
107 * Add a cell tool item.
108 */
109 addItem(options: NotebookTools.IAddOptions): void {
110 const tool = options.tool;
111 const rank = options.rank ?? 100;
112
113 let section: RankedPanel<NotebookTools.Tool>;
114 const extendedTool = this._tools.find(
115 extendedTool => extendedTool.section === options.section
116 );
117 if (extendedTool) section = extendedTool.panel;
118 else {
119 throw new Error(`The section ${options.section} does not exist`);
120 }
121
122 tool.addClass('jp-NotebookTools-tool');
123 section.addWidget(tool, rank);
124 // TODO: perhaps the necessary notebookTools functionality should be
125 // consolidated into a single object, rather than a broad reference to this.
126 tool.notebookTools = this;
127
128 // Trigger the tool to update its active notebook and cell.
129 MessageLoop.sendMessage(tool, NotebookTools.ActiveNotebookPanelMessage);
130 MessageLoop.sendMessage(tool, NotebookTools.ActiveCellMessage);
131 }
132
133 /*
134 * Add a section to the notebook tool with its widget
135 */
136 addSection(options: NotebookTools.IAddSectionOptions): void {
137 const sectionName = options.sectionName;
138 const label = options.label || options.sectionName;
139 const widget = options.tool;
140 let rank = options.rank ?? null;
141
142 const newSection = new RankedPanel<NotebookTools.Tool>();
143 newSection.title.label = label;
144
145 if (widget) newSection.addWidget(widget, 0);
146
147 this._tools.push({
148 section: sectionName,
149 panel: newSection,
150 rank: rank
151 });
152
153 if (rank != null)
154 (this.layout as PanelLayout).insertWidget(
155 rank,
156 new Collapser({ widget: newSection })
157 );
158 else {
159 // If no rank is provided, try to add the new section before the AdvancedTools.
160 let advancedToolsRank = null;
161 const layout = this.layout as PanelLayout;
162 for (let i = 0; i < layout.widgets.length; i++) {
163 let w = layout.widgets[i];
164 if (w instanceof Collapser) {
165 if (w.widget.id === 'advancedToolsSection') {
166 advancedToolsRank = i;
167 break;
168 }
169 }
170 }
171
172 if (advancedToolsRank !== null)
173 (this.layout as PanelLayout).insertWidget(
174 advancedToolsRank,
175 new Collapser({ widget: newSection })
176 );
177 else
178 (this.layout as PanelLayout).addWidget(
179 new Collapser({ widget: newSection })
180 );
181 }
182 }
183
184 /**
185 * Handle a change to the notebook panel.
186 */
187 private _onActiveNotebookPanelChanged(): void {
188 if (
189 this._prevActiveNotebookModel &&
190 !this._prevActiveNotebookModel.isDisposed
191 ) {
192 this._prevActiveNotebookModel.metadataChanged.disconnect(
193 this._onActiveNotebookPanelMetadataChanged,
194 this
195 );
196 }
197 const activeNBModel =
198 this.activeNotebookPanel && this.activeNotebookPanel.content
199 ? this.activeNotebookPanel.content.model
200 : null;
201 this._prevActiveNotebookModel = activeNBModel;
202 if (activeNBModel) {
203 activeNBModel.metadataChanged.connect(
204 this._onActiveNotebookPanelMetadataChanged,
205 this
206 );
207 }
208 for (const widget of this._toolChildren()) {
209 MessageLoop.sendMessage(widget, NotebookTools.ActiveNotebookPanelMessage);
210 }
211 }
212
213 /**
214 * Handle a change to the active cell.
215 */
216 private _onActiveCellChanged(): void {
217 if (this._prevActiveCell && !this._prevActiveCell.isDisposed) {
218 this._prevActiveCell.metadataChanged.disconnect(
219 this._onActiveCellMetadataChanged,
220 this
221 );
222 }
223 const activeCell = this.activeCell ? this.activeCell.model : null;
224 this._prevActiveCell = activeCell;
225 if (activeCell) {
226 activeCell.metadataChanged.connect(
227 this._onActiveCellMetadataChanged,
228 this
229 );
230 }
231 for (const widget of this._toolChildren()) {
232 MessageLoop.sendMessage(widget, NotebookTools.ActiveCellMessage);
233 }
234 }
235
236 /**
237 * Handle a change in the selection.
238 */
239 private _onSelectionChanged(): void {
240 for (const widget of this._toolChildren()) {
241 MessageLoop.sendMessage(widget, NotebookTools.SelectionMessage);
242 }
243 }
244
245 /**
246 * Handle a change in the active cell metadata.
247 */
248 private _onActiveNotebookPanelMetadataChanged(
249 sender: INotebookModel,
250 args: IMapChange
251 ): void {
252 const message = new ObservableJSON.ChangeMessage(
253 'activenotebookpanel-metadata-changed',
254 { oldValue: undefined, newValue: undefined, ...args }
255 );
256 for (const widget of this._toolChildren()) {
257 MessageLoop.sendMessage(widget, message);
258 }
259 }
260
261 /**
262 * Handle a change in the notebook model metadata.
263 */
264 private _onActiveCellMetadataChanged(
265 sender: ICellModel,
266 args: IMapChange
267 ): void {
268 const message = new ObservableJSON.ChangeMessage(
269 'activecell-metadata-changed',
270 { newValue: undefined, oldValue: undefined, ...args }
271 );
272 for (const widget of this._toolChildren()) {
273 MessageLoop.sendMessage(widget, message);
274 }
275 }
276
277 private *_toolChildren() {
278 for (let tool of this._tools) {
279 yield* tool.panel.children();
280 }
281 }
282
283 translator: ITranslator;
284 private _tools: Array<NotebookTools.IToolPanel>;
285 private _tracker: INotebookTracker;
286 private _prevActiveCell: ICellModel | null;
287 private _prevActiveNotebookModel: INotebookModel | null;
288}
289
290/**
291 * The namespace for NotebookTools class statics.
292 */
293export namespace NotebookTools {
294 /**
295 * A type alias for a readonly partial JSON tuples `[option, value]`.
296 * `option` should be localized.
297 *
298 * Note: Partial here means that JSON object attributes can be `undefined`.
299 */
300 export type ReadonlyPartialJSONOptionValueArray = [
301 ReadonlyPartialJSONValue | undefined,
302 ReadonlyPartialJSONValue
303 ][];
304
305 /**
306 * Interface for an extended panel section.
307 */
308 export interface IToolPanel {
309 /**
310 * The name of the section.
311 */
312 section: string;
313
314 /**
315 * The associated panel, only one for a section.
316 */
317 panel: RankedPanel<NotebookTools.Tool>;
318
319 /**
320 * The rank of the section on the notebooktools panel.
321 */
322 rank?: number | null;
323 }
324
325 /**
326 * The options used to create a NotebookTools object.
327 */
328 export interface IOptions {
329 /**
330 * The notebook tracker used by the notebook tools.
331 */
332 tracker: INotebookTracker;
333
334 /**
335 * Language translator.
336 */
337 translator?: ITranslator;
338 }
339
340 /**
341 * The options used to add an item to the notebook tools.
342 */
343 export interface IAddOptions {
344 /**
345 * The tool to add to the notebook tools area.
346 */
347 tool: INotebookTools.ITool;
348
349 /**
350 * The section to which the tool should be added.
351 */
352 section: string;
353
354 /**
355 * The rank order of the widget among its siblings.
356 */
357 rank?: number;
358 }
359
360 /**
361 * The options used to add a section to the notebook tools.
362 */
363 export interface IAddSectionOptions {
364 /**
365 * The name of the new section.
366 */
367 sectionName: string;
368
369 /**
370 * The tool to add to the notebook tools area.
371 */
372 tool?: INotebookTools.ITool;
373
374 /**
375 * The label of the new section.
376 */
377 label?: string;
378
379 /**
380 * The rank order of the section among its siblings.
381 */
382 rank?: number;
383 }
384
385 /**
386 * A singleton conflatable `'activenotebookpanel-changed'` message.
387 */
388 export const ActiveNotebookPanelMessage = new ConflatableMessage(
389 'activenotebookpanel-changed'
390 );
391
392 /**
393 * A singleton conflatable `'activecell-changed'` message.
394 */
395 export const ActiveCellMessage = new ConflatableMessage('activecell-changed');
396
397 /**
398 * A singleton conflatable `'selection-changed'` message.
399 */
400 export const SelectionMessage = new ConflatableMessage('selection-changed');
401
402 /**
403 * The base notebook tool, meant to be subclassed.
404 */
405 export class Tool extends Widget implements INotebookTools.ITool {
406 /**
407 * The notebook tools object.
408 */
409 notebookTools: INotebookTools;
410
411 dispose(): void {
412 super.dispose();
413 if (this.notebookTools) {
414 this.notebookTools = null!;
415 }
416 }
417
418 /**
419 * Process a message sent to the widget.
420 *
421 * @param msg - The message sent to the widget.
422 */
423 processMessage(msg: Message): void {
424 super.processMessage(msg);
425 switch (msg.type) {
426 case 'activenotebookpanel-changed':
427 this.onActiveNotebookPanelChanged(msg);
428 break;
429 case 'activecell-changed':
430 this.onActiveCellChanged(msg);
431 break;
432 case 'selection-changed':
433 this.onSelectionChanged(msg);
434 break;
435 case 'activecell-metadata-changed':
436 this.onActiveCellMetadataChanged(msg as ObservableJSON.ChangeMessage);
437 break;
438 case 'activenotebookpanel-metadata-changed':
439 this.onActiveNotebookPanelMetadataChanged(
440 msg as ObservableJSON.ChangeMessage
441 );
442 break;
443 default:
444 break;
445 }
446 }
447
448 /**
449 * Handle a change to the notebook panel.
450 *
451 * #### Notes
452 * The default implementation is a no-op.
453 */
454 protected onActiveNotebookPanelChanged(msg: Message): void {
455 /* no-op */
456 }
457
458 /**
459 * Handle a change to the active cell.
460 *
461 * #### Notes
462 * The default implementation is a no-op.
463 */
464 protected onActiveCellChanged(msg: Message): void {
465 /* no-op */
466 }
467
468 /**
469 * Handle a change to the selection.
470 *
471 * #### Notes
472 * The default implementation is a no-op.
473 */
474 protected onSelectionChanged(msg: Message): void {
475 /* no-op */
476 }
477
478 /**
479 * Handle a change to the metadata of the active cell.
480 *
481 * #### Notes
482 * The default implementation is a no-op.
483 */
484 protected onActiveCellMetadataChanged(
485 msg: ObservableJSON.ChangeMessage
486 ): void {
487 /* no-op */
488 }
489
490 /**
491 * Handle a change to the metadata of the active cell.
492 *
493 * #### Notes
494 * The default implementation is a no-op.
495 */
496 protected onActiveNotebookPanelMetadataChanged(
497 msg: ObservableJSON.ChangeMessage
498 ): void {
499 /* no-op */
500 }
501 }
502
503 /**
504 * A raw metadata editor.
505 */
506 export class MetadataEditorTool extends Tool {
507 /**
508 * Construct a new raw metadata tool.
509 */
510 constructor(options: MetadataEditorTool.IOptions) {
511 super();
512 const { editorFactory } = options;
513 this.addClass('jp-MetadataEditorTool');
514 const layout = (this.layout = new PanelLayout());
515
516 this._editorFactory = editorFactory;
517 this._editorLabel = options.label || 'Edit Metadata';
518 this.createEditor();
519 const titleNode = new Widget({ node: document.createElement('label') });
520 titleNode.node.textContent = options.label || 'Edit Metadata';
521 layout.addWidget(titleNode);
522 layout.addWidget(this.editor);
523 }
524
525 /**
526 * The editor used by the tool.
527 */
528 get editor(): JSONEditor {
529 return this._editor;
530 }
531
532 /**
533 * Handle a change to the notebook.
534 */
535 protected onActiveNotebookPanelChanged(msg: Message): void {
536 this.editor.dispose();
537 if (this.notebookTools.activeNotebookPanel) {
538 this.createEditor();
539 }
540 }
541
542 protected createEditor() {
543 this._editor = new JSONEditor({
544 editorFactory: this._editorFactory
545 });
546 this.editor.title.label = this._editorLabel;
547
548 (this.layout as PanelLayout).addWidget(this.editor);
549 }
550
551 private _editor: JSONEditor;
552 private _editorLabel: string;
553 private _editorFactory: CodeEditor.Factory;
554 }
555
556 /**
557 * The namespace for `MetadataEditorTool` static data.
558 */
559 export namespace MetadataEditorTool {
560 /**
561 * The options used to initialize a metadata editor tool.
562 */
563 export interface IOptions {
564 /**
565 * The editor factory used by the tool.
566 */
567 editorFactory: CodeEditor.Factory;
568
569 /**
570 * The label for the JSON editor
571 */
572 label?: string;
573
574 /**
575 * Initial collapse state, defaults to true.
576 */
577 collapsed?: boolean;
578
579 /**
580 * Language translator.
581 */
582 translator?: ITranslator;
583 }
584 }
585}
586
587/**
588 * A namespace for private data.
589 */
590namespace Private {
591 /**
592 * An object which holds a widget and its sort rank.
593 */
594 export interface IRankItem<T extends Widget = Widget> {
595 /**
596 * The widget for the item.
597 */
598 widget: T;
599
600 /**
601 * The sort rank of the menu.
602 */
603 rank: number;
604 }
605
606 /**
607 * A comparator function for widget rank items.
608 */
609 export function itemCmp(first: IRankItem, second: IRankItem): number {
610 return first.rank - second.rank;
611 }
612}