Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for printing widgets #5850

Merged
merged 45 commits into from May 13, 2019

Conversation

saulshanabrook
Copy link
Member

@saulshanabrook saulshanabrook commented Jan 9, 2019

This closes #1314 by adding the ability to print different widgets, including notebooks.

Features

It adds a new command called Print... to the File menu, which is enabled when the active widget is printable. This is enabled currently for:

  • images (copies dom nodes)
  • the inspector (copies dom nodes)
  • notebooks (loads nbformat URL prints out HTML output)
  • JSON viewer (copies dom nodes)

This is set to SUPER P by default.

Not implemented

  • a button for printing the current widget
  • A keyboard shortcut for printing the current widget

Implementation

The core printing logic is in the apputils/src/printing.tsx file, with the core structure being the IPrintable interface:

export type OptionalAsyncThunk = () => Promise<void> | null;
export const printSymbol = Symbol();
export interface IPrintable {
  [symbol]: () => OptionalAsyncThunk;
}

The command checks to see if the active widget is printable (i.e. it has the printSymbol property and calling that method returns a function) and if so calls that method on it.

There are some helper functions provided to support having widgets defer their ability to print to a child widget and the ability to print a widgets DOM node or a custom URL.

To print a DOM node, or a URL, we create an iframe and copy in a DOM node into it or set the src text to HTML text.

Screenshots

Notebook:

screen shot 2019-01-09 at 4 01 38 pm

Inspector:

screen shot 2019-01-09 at 2 00 33 pm

JSON:

Image:

@saulshanabrook saulshanabrook changed the title Add supporting for printing widgets WIP: Add supporting for printing widgets Jan 9, 2019
@saulshanabrook saulshanabrook changed the title WIP: Add supporting for printing widgets WIP: Add support for printing widgets Jan 9, 2019
@saulshanabrook saulshanabrook added this to the 1.0 milestone Jan 9, 2019
@jasongrout
Copy link
Contributor

Are you tying print functionality to document widgets (I notice it seems some functionality is on the docmanager)? It would be great if an arbitrary activity could be printable, even if it wasn't a document widget.

@jasongrout
Copy link
Contributor

(An example of an interesting printable non-document widget might be the inspector panel)

@saulshanabrook
Copy link
Member Author

Are you tying print functionality to document widgets (I notice it seems some functionality is on the docmanager)? It would be great if an arbitrary activity could be printable, even if it wasn't a document widget.

Nope. It call the printSymbol method on the widget, it isn't tied to the document widget. Check out the latest commit, in it I have printing working for images, the inspector, and JSON.

screen shot 2019-01-09 at 2 00 33 pm

@jasongrout
Copy link
Contributor

Awesome. I'm excited about this work!

Also, I'm glad you are experimenting with using Symbols.

@saulshanabrook saulshanabrook changed the title WIP: Add support for printing widgets Add support for printing widgets Jan 9, 2019
@saulshanabrook
Copy link
Member Author

OK this is ready to go from my end.

@jasongrout jasongrout self-requested a review January 27, 2019 05:56
Copy link
Contributor

@jasongrout jasongrout left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ian-r-rose - can you comment on the security of printing via iframe? I notice that https://github.com/joseluisq/printd/blob/master/src/index.ts doesn't seem to use any sandbox attributes.

packages/apputils/src/printing.ts Outdated Show resolved Hide resolved
packages/apputils/src/printing.ts Outdated Show resolved Hide resolved
packages/apputils/src/printing.ts Outdated Show resolved Hide resolved
/**
* Prints a URL by loading it into an iframe.
*
* NOTE: When https://github.com/joseluisq/printd/issues/20 is fixed this can be removed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our convention is to add a #### Notes section after the parameters for notes like these.

Copy link
Member

@ian-r-rose ian-r-rose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unable to get this to build, but the overall approach looks good. Looks like it just needs a rebase.

packages/apputils/src/printing.ts Outdated Show resolved Hide resolved
* Print in iframe
*/
[printSymbol]() {
printWidget.bind(this)('.p-mod-hidden {display: none;}');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to grab more CSS so that this is less ad hoc? I admit this is stretching the bounds of my DOM API knowledge, but if we could do something like node.getAllCSSAsString(), that would be much nicer.

const factory = new NotebookWidgetFactory({
name: FACTORY,
fileTypes: ['notebook'],
modelName: 'notebook',
defaultFor: ['notebook'],
preferKernel: true,
baseUrl: services.serverSettings.baseUrl,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the notebook widget already has access to the serviceManager, so it shouldn't be necessary to pass in the baseUrl individually.

@@ -475,12 +470,14 @@ function activateNotebookHandler(
// An object for tracking the current notebook settings.
let editorConfig = StaticNotebook.defaultEditorConfig;
let notebookConfig = StaticNotebook.defaultNotebookConfig;
// Pass URL in here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of date comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks!

packages/notebook/src/panel.ts Outdated Show resolved Hide resolved
packages/notebook/src/widget.ts Outdated Show resolved Hide resolved
@saulshanabrook
Copy link
Member Author

Thanks @ian-r-rose and @jasongrout for the comments, much appreciated.

@saulshanabrook
Copy link
Member Author

In reference to @ian-r-rose's question about including the base URL in the NotebookPanel, we had a productive discussion during our meeting today about how to avoid this problem.

Solutions:

  • Move getNBConvertURL to page config file and import it in notebook panel to define printing. There are other similar URL utilities in page config so it makes some sense there and then we don't have to have it passed in. However, the downside of this is that the nbconvert server might not be running, and should probably be optional, and this exposes it as a global core API.
  • This thinking encourages us to make nbconvert a separate extension, not tied to the notebook. If this is the case, we need to be able to add the printing logic outside of the NotebookPanel class. We could do this by:
    • Keeping existing interface based print API:
      • Monkeypatch NotebookPanel class to add print symbol property
      • Monkeypatch each NotebookPanel instance by registering some notebook widget tracker (@ian-r-rose I forget the actual name) on instantiate.
    • Change the print API to also include a dispatch based approach. This would allow the (hypothetical) NB convert plugin to say "I know how to print NotebookPanel widgets and this is how".

I am currently in favor of the last option (dispatching), because it seems the most general, but also allowing widgets to define their own printing. I will keep exploring this and post possible designs here.

@ian-r-rose
Copy link
Member

ian-r-rose commented Jan 30, 2019

    * Monkeypatch each `NotebookPanel` instance by registering some notebook widget tracker (@ian-r-rose I forget the actual name) on instantiate.

I was referring to a DocumentRegistry.IWidgetExtension:

/**
* An interface for a widget extension.
*/
export interface IWidgetExtension<T extends Widget, U extends IModel> {
/**
* Create a new extension for a given widget.
*/
createNew(widget: T, context: IContext<U>): IDisposable;

This, for instance, is what the ipywidgets extension uses to add the widgets mimetype to a notebook:
https://github.com/jupyter-widgets/ipywidgets/blob/1f93ad39802d3ee4a6b88bc13064f93f8f796374/packages/jupyterlab-manager/src/plugin.ts#L136

Another option would be to require the INotebookTracker and hook into the widgetAdded signal to patch on the print symbol. This would essentially accomplish the same thing. So much so, that we have been considering removing the DocumentRegistry.IWidgetExtension (#3974).

@gnestor
Copy link
Contributor

gnestor commented Feb 1, 2019

@saulshanabrook Do you have any insight into what it would require for nbconvert to output static HTML of ipywidget output? There is a conversation about this going on as jupyter/nbconvert#944

@saulshanabrook
Copy link
Member Author

@gnestor, I do not, but thanks for the pointer.

@bollwyvl
Copy link
Contributor

bollwyvl commented Feb 1, 2019 via email

@gnestor
Copy link
Contributor

gnestor commented Feb 2, 2019

@bollwyvl Yes, it does sound like we would need to use something like decktape that can render the output in a headless browser and convert that to a static HTML, image, PDF, etc.

I think if we were to do anything like this as jupyter, it would have to
support/prefer Firefox.

Why is this?

It's a little "I heard you liked widgets", but it seems like widget
packages would also have to include a kernel-side html renderer to enable
this without a browser in the loop... For some things, this would just mean
packing an AMD package (for now) that nbconvert could find and handle prior
to interpreting the widget... unpkg etc is not a robust solution for
archival content. some widgets could opt for full server-side rendering.

The simple solution is that widgets provide fallback mimetypes in the output, like text/html or image/png. Do you envision the "kernel-side html renderer" providing a text/html output?

@bollwyvl
Copy link
Contributor

bollwyvl commented Feb 2, 2019 via email

@gnestor
Copy link
Contributor

gnestor commented Feb 6, 2019

However, if you could keep interactivity, you might need to ship some js...

The one issue with this is that widgets typically interact with a kernel (e.g. that's what differentiates the plotlywidget from the plotly-extension), so when I see a widget rendered outside of a notebook but it doesn't behave the way it's expected to because it's not connected to a kernel (like in the ipywidgets docs: https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Events.html#Linking-traitlets-attributes-in-the-kernel), then I think the result is worse than if it were just a static image.

@bollwyvl
Copy link
Contributor

bollwyvl commented Feb 6, 2019 via email

@saulshanabrook
Copy link
Member Author

@ian-r-rose and I did some pair coding on this today and ironed out most of the kinks. Currently, the issue is that one Safari and on Chrome on linux (but not on Mac) the iframe is not being cleaned up because the afterprint event isn't being triggered for some reason.

Ian is gonna look into this a bit more and maybe experiment with watching how the media queries change as a proxy for when we are done printing.

Another option is to only wait for afterprint on firefox, because on chrome it seems to block anyways for long enough to keep the print dialogue open before deleting the iframe.

@ian-r-rose
Copy link
Member

Should have been obvious to us earlier, but the reason that onafterprint and media queries were not behaving as we expected was due to the fact that we dont 'allow-scripts'.

@ian-r-rose
Copy link
Member

@saulshanabrook, do you have any thoughts on how to test this in CI?

JS is blocked, we consider it safe to discard of the hidden iframe once
event handling has returned to the main content window.
@ian-r-rose
Copy link
Member

@saulshanabrook I've added what might be considered a huge hack for deciding when to discard the hidden iframe, but it seems to work okay on Ubuntu with Chrome and Firefox. What do you think?

Copy link
Member

@ian-r-rose ian-r-rose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is really close. Do you have any thoughts on how we would get this working for codemirror instances?

packages/apputils/src/printing.ts Outdated Show resolved Hide resolved
* Copies a node from the base document to the iframe.
*/
function setIFrameNode(iframe: HTMLIFrameElement, node: HTMLElement) {
iframe.contentDocument.body.appendChild(node.cloneNode(true));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to wait for the load event? My experience with the PDF viewer is that it was necessary, but maybe that's not the case here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't found we needed to wait.

* Returns the URL converting this notebook to a certain
* format with nbconvert.
*/
export function getNBConvertURL({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping @afshin on this

@saulshanabrook
Copy link
Member Author

saulshanabrook commented May 13, 2019

Should have been obvious to us earlier, but the reason that onafterprint and media queries were not behaving as we expected was due to the fact that we dont 'allow-scripts'.

But they do trigger in Chrome, which is odd...

I've added what might be considered a huge hack for deciding when to discard the hidden iframe, but it seems to work okay on Ubuntu with Chrome and Firefox. What do you think?

Looks good to me! I will test it locally in chrome, firefox and safari to make sure it is cleaned up.

@saulshanabrook, do you have any thoughts on how to test this in CI?

Hah... Selenium doesn't cover printing. Looks like our best bet would be to use puppeteer, Chrome's new browser UX testing framework, but it looks like the print dialogue isn't caught yet (puppeteer/puppeteer#3145). They are also starting a FIrefox compatible puppeteer.

Do you have any thoughts on how we would get this working for codemirror instances?

I will have a go at it.

@saulshanabrook
Copy link
Member Author

I will test it locally in chrome, firefox and safari to make sure it is cleaned up.

I tested printing a notebook and the inspector on all three, to test the two code paths. They all printed properly and the iframes were cleaned up, besides in safari, printing the inspector resulted in a blank screen. I am fine to call that "success"

@saulshanabrook, do you have any thoughts on how to test this in CI?

Maybe I could document how to test this for a manual release sort of test? I think there are just some UX tests that cannot be automated currently and the best practice would be to have a list we could go down to verify that they are working. This slightly overlaps with @blink1073's work in #6312. Do you think I should put something like that in the RELEASE.md file? cc @jasongrout and @ellisonbg

@saulshanabrook
Copy link
Member Author

Do you have any thoughts on how we would get this working for codemirror instances?

I started trying to get this working, but kept ending up with an unstyled printout and am finding it hard to debug:

Screen Shot 2019-05-13 at 8 34 35 AM

Here is the commit that is trying this out. I get rather confused about when I should be waiting for the page to load. If you wanted to try to work through this together this week, I would be game, or happy to merge this as is.

@saulshanabrook
Copy link
Member Author

The docs failure is because a github URL is not resolving properly temporarily, it says "too many hits..."

@ian-r-rose
Copy link
Member

I will test it locally in chrome, firefox and safari to make sure it is cleaned up.

I tested printing a notebook and the inspector on all three, to test the two code paths. They all printed properly and the iframes were cleaned up, besides in safari, printing the inspector resulted in a blank screen. I am fine to call that "success"

Does this mean that printing DOM elements doesn't work in Safari? Did it work with any of the previous iterations of the PR?

@ian-r-rose
Copy link
Member

This looks great, thanks for the perseverance @saulshanabrook!

@ian-r-rose ian-r-rose merged commit 3670ef1 into jupyterlab:master May 13, 2019
@saulshanabrook saulshanabrook deleted the print-notebook branch May 13, 2019 17:49
@lock lock bot added the status:resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion. label Aug 7, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Aug 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement pkg:apputils pkg:json pkg:notebook status:resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion. tag:Browser Compatibility
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handle printing of documents in browser
7 participants