Skip to content

Commit

Permalink
Merge pull request #5981 from ian-r-rose/restore-cloned-output
Browse files Browse the repository at this point in the history
Restore cloned output
  • Loading branch information
afshin committed Feb 14, 2019
2 parents 3f908a8 + bf70f2c commit 61fe9f2
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 35 deletions.
59 changes: 43 additions & 16 deletions packages/apputils/src/instancetracker.ts
Expand Up @@ -7,7 +7,7 @@ import { ArrayExt, each, find } from '@phosphor/algorithm';

import { CommandRegistry } from '@phosphor/commands';

import { ReadonlyJSONObject } from '@phosphor/coreutils';
import { PromiseDelegate, ReadonlyJSONObject } from '@phosphor/coreutils';

import { IDisposable } from '@phosphor/disposable';

Expand Down Expand Up @@ -52,6 +52,19 @@ export interface IInstanceTracker<T extends Widget> extends IDisposable {
*/
readonly size: number;

/**
* A promise that is resolved when the instance tracker has been
* restored from a serialized state.
*
* #### Notes
* Most client code will not need to use this, since they can wait
* for the whole application to restore. However, if an extension
* wants to perform actions during the application restoration, but
* after the restoration of another instance tracker, they can use
* this promise.
*/
readonly restored: Promise<void>;

/**
* Find the first widget in the tracker that satisfies a filter function.
*
Expand Down Expand Up @@ -325,7 +338,11 @@ export class InstanceTracker<T extends Widget>
* multiple instance trackers and, when ready, asks them each to restore their
* respective widgets.
*/
restore(options: InstanceTracker.IRestoreOptions<T>): Promise<any> {
async restore(options: InstanceTracker.IRestoreOptions<T>): Promise<any> {
if (this._hasRestored) {
throw new Error('Instance tracker has already restored');
}
this._hasRestored = true;
const { command, registry, state, when } = options;
const namespace = this.namespace;
const promises = when
Expand All @@ -334,20 +351,28 @@ export class InstanceTracker<T extends Widget>

this._restore = options;

return Promise.all(promises).then(([saved]) => {
return Promise.all(
saved.ids.map((id, index) => {
const value = saved.values[index];
const args = value && (value as any).data;
if (args === undefined) {
return state.remove(id);
}

// Execute the command and if it fails, delete the state restore data.
return registry.execute(command, args).catch(() => state.remove(id));
})
);
});
const [saved] = await Promise.all(promises);
const values = await Promise.all(
saved.ids.map((id, index) => {
const value = saved.values[index];
const args = value && (value as any).data;
if (args === undefined) {
return state.remove(id);
}

// Execute the command and if it fails, delete the state restore data.
return registry.execute(command, args).catch(() => state.remove(id));
})
);
this._restored.resolve(undefined);
return values;
}

/**
* A promise resolved when the instance tracker has been restored.
*/
get restored(): Promise<void> {
return this._restored.promise;
}

/**
Expand Down Expand Up @@ -447,7 +472,9 @@ export class InstanceTracker<T extends Widget>
}
}

private _hasRestored = false;
private _restore: InstanceTracker.IRestoreOptions<T> | null = null;
private _restored = new PromiseDelegate<void>();
private _tracker = new FocusTracker<T>();
private _currentChanged = new Signal<this, T | null>(this);
private _widgetAdded = new Signal<this, T>(this);
Expand Down
2 changes: 2 additions & 0 deletions packages/notebook-extension/package.json
Expand Up @@ -36,13 +36,15 @@
"@jupyterlab/cells": "^1.0.0-alpha.3",
"@jupyterlab/codeeditor": "^1.0.0-alpha.3",
"@jupyterlab/coreutils": "^3.0.0-alpha.3",
"@jupyterlab/docmanager": "^1.0.0-alpha.3",
"@jupyterlab/filebrowser": "^1.0.0-alpha.3",
"@jupyterlab/launcher": "^1.0.0-alpha.3",
"@jupyterlab/mainmenu": "^1.0.0-alpha.3",
"@jupyterlab/notebook": "^1.0.0-alpha.4",
"@jupyterlab/rendermime": "^1.0.0-alpha.3",
"@jupyterlab/services": "^4.0.0-alpha.3",
"@jupyterlab/statusbar": "^1.0.0-alpha.3",
"@phosphor/algorithm": "^1.1.2",
"@phosphor/coreutils": "^1.3.0",
"@phosphor/disposable": "^1.1.2",
"@phosphor/messaging": "^1.2.2",
Expand Down
168 changes: 149 additions & 19 deletions packages/notebook-extension/src/index.ts
Expand Up @@ -11,6 +11,7 @@ import {
import {
Dialog,
ICommandPalette,
InstanceTracker,
MainAreaWidget,
showDialog
} from '@jupyterlab/apputils';
Expand All @@ -26,6 +27,10 @@ import {
URLExt
} from '@jupyterlab/coreutils';

import { IDocumentManager } from '@jupyterlab/docmanager';

import { ArrayExt } from '@phosphor/algorithm';

import { UUID } from '@phosphor/coreutils';

import { DisposableSet } from '@phosphor/disposable';
Expand Down Expand Up @@ -68,7 +73,7 @@ import { ReadonlyJSONObject, JSONValue } from '@phosphor/coreutils';

import { Message, MessageLoop } from '@phosphor/messaging';

import { Menu } from '@phosphor/widgets';
import { Panel, Menu } from '@phosphor/widgets';

/**
* The command IDs used by the notebook plugin.
Expand Down Expand Up @@ -251,6 +256,7 @@ const trackerPlugin: JupyterFrontEndPlugin<INotebookTracker> = {
provides: INotebookTracker,
requires: [
NotebookPanel.IContentFactory,
IDocumentManager,
IEditorServices,
IRenderMimeRegistry
],
Expand Down Expand Up @@ -480,6 +486,7 @@ function activateCellTools(
function activateNotebookHandler(
app: JupyterFrontEnd,
contentFactory: NotebookPanel.IContentFactory,
docManager: IDocumentManager,
editorServices: IEditorServices,
rendermime: IRenderMimeRegistry,
palette: ICommandPalette | null,
Expand Down Expand Up @@ -508,6 +515,11 @@ function activateNotebookHandler(
});
const { commands } = app;
const tracker = new NotebookTracker({ namespace: 'notebook' });
const clonedOutputs = new InstanceTracker<
MainAreaWidget<Private.ClonedOutputArea>
>({
namespace: 'cloned-outputs'
});

// Handle state restoration.
if (restorer) {
Expand All @@ -517,13 +529,22 @@ function activateNotebookHandler(
name: panel => panel.context.path,
when: services.ready
});
restorer.restore(clonedOutputs, {
command: CommandIDs.createOutputView,
args: widget => ({
path: widget.content.path,
index: widget.content.index
}),
name: widget => `${widget.content.path}:${widget.content.index}`,
when: tracker.restored // After the notebook widgets (but not contents).
});
}

let registry = app.docRegistry;
registry.addModelFactory(new NotebookModelFactory({}));
registry.addWidgetFactory(factory);

addCommands(app, services, tracker);
addCommands(app, docManager, services, tracker, clonedOutputs);
if (palette) {
populatePalette(palette, services);
}
Expand Down Expand Up @@ -815,8 +836,10 @@ function activateNotebookHandler(
*/
function addCommands(
app: JupyterFrontEnd,
docManager: IDocumentManager,
services: ServiceManager,
tracker: NotebookTracker
tracker: NotebookTracker,
clonedOutputs: InstanceTracker<MainAreaWidget>
): void {
const { commands, shell } = app;

Expand Down Expand Up @@ -1487,30 +1510,53 @@ function addCommands(
});
commands.addCommand(CommandIDs.createOutputView, {
label: 'Create New View for Output',
execute: args => {
// Clone the OutputArea
const current = getCurrent({ ...args, activate: false });
const nb = current.content;
const content = (nb.activeCell as CodeCell).cloneOutputArea();
execute: async args => {
let cell: CodeCell | undefined;
let current: NotebookPanel | undefined;
// If we are given a notebook path and cell index, then
// use that, otherwise use the current active cell.
let path = args.path as string | undefined | null;
let index = args.index as number | undefined | null;
if (path && index !== undefined && index !== null) {
current = docManager.findWidget(path, FACTORY) as NotebookPanel;
if (!current) {
return;
}
} else {
current = getCurrent({ ...args, activate: false });
if (!current) {
return;
}
cell = current.content.activeCell as CodeCell;
index = current.content.activeCellIndex;
}
// Create a MainAreaWidget
const content = new Private.ClonedOutputArea({
notebook: current,
cell,
index
});
const widget = new MainAreaWidget({ content });
widget.id = `LinkedOutputView-${UUID.uuid4()}`;
widget.title.label = 'Output View';
widget.title.icon = NOTEBOOK_ICON_CLASS;
widget.title.caption = current.title.label
? `For Notebook: ${current.title.label}`
: 'For Notebook:';
widget.addClass('jp-LinkedOutputView');
current.context.addSibling(widget, {
ref: current.id,
mode: 'split-bottom'
});

const updateCloned = () => {
clonedOutputs.save(widget);
};
current.context.pathChanged.connect(updateCloned);
current.content.model.cells.changed.connect(updateCloned);

// Add the cloned output to the output instance tracker.
clonedOutputs.add(widget);

// Remove the output view if the parent notebook is closed.
nb.disposed.connect(
widget.dispose,
widget
);
current.content.disposed.connect(() => {
current.context.pathChanged.disconnect(updateCloned);
current.content.model.cells.changed.disconnect(updateCloned);
widget.dispose();
});
},
isEnabled: isEnabledAndSingleSelected
});
Expand Down Expand Up @@ -2093,3 +2139,87 @@ function populateMenus(
getKernel: current => current.session.kernel
} as IHelpMenu.IKernelUser<NotebookPanel>);
}

/**
* A namespace for module private functionality.
*/
namespace Private {
/**
* A widget hosting a cloned output area.
*/
export class ClonedOutputArea extends Panel {
constructor(options: ClonedOutputArea.IOptions) {
super();
this._notebook = options.notebook;
this._index = options.index !== undefined ? options.index : -1;
this._cell = options.cell || null;
this.id = `LinkedOutputView-${UUID.uuid4()}`;
this.title.label = 'Output View';
this.title.icon = NOTEBOOK_ICON_CLASS;
this.title.caption = this._notebook.title.label
? `For Notebook: ${this._notebook.title.label}`
: 'For Notebook:';
this.addClass('jp-LinkedOutputView');

// Wait for the notebook to be loaded before
// cloning the output area.
this._notebook.context.ready.then(() => {
if (!this._cell) {
this._cell = this._notebook.content.widgets[this._index] as CodeCell;
}
if (!this._cell || this._cell.model.type !== 'code') {
this.dispose();
return;
}
const clone = this._cell.cloneOutputArea();
this.addWidget(clone);
});
}

/**
* The index of the cell in the notebook.
*/
get index(): number {
return this._cell
? ArrayExt.findFirstIndex(
this._notebook.content.widgets,
c => c === this._cell
)
: this._index;
}

/**
* The path of the notebook for the cloned output area.
*/
get path(): string {
return this._notebook.context.path;
}

private _notebook: NotebookPanel;
private _index: number;
private _cell: CodeCell | null = null;
}

/**
* ClonedOutputArea statics.
*/
export namespace ClonedOutputArea {
export interface IOptions {
/**
* The notebook associated with the cloned output area.
*/
notebook: NotebookPanel;

/**
* The cell for which to clone the output area.
*/
cell?: CodeCell;

/**
* If the cell is not available, provide the index
* of the cell for when the notebook is loaded.
*/
index?: number;
}
}
}
3 changes: 3 additions & 0 deletions packages/notebook-extension/tsconfig.json
Expand Up @@ -21,6 +21,9 @@
{
"path": "../coreutils"
},
{
"path": "../docmanager"
},
{
"path": "../filebrowser"
},
Expand Down

0 comments on commit 61fe9f2

Please sign in to comment.