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 UI modal dialog for saving plots #2589

Merged
merged 10 commits into from
Apr 4, 2024
Merged

Add UI modal dialog for saving plots #2589

merged 10 commits into from
Apr 4, 2024

Conversation

timtmok
Copy link
Contributor

@timtmok timtmok commented Apr 1, 2024

Intent

Address #2169

Approach

For static plots, present a file dialog to select the save location. It saves the data uri that is shown in the plots pane.

For dynamic plots, a dialog is presented to select the save location, width, height, and DPI. A preview is available if the user wants to see what it looks like before saving. At save, the plot is rendered and saved.

The modal dialog uses callbacks as was changed with the recent refactoring with modals.

The file formats was mostly scaffolding for future support and this continues that with a bit of hardcoding to PNG.

image

QA Notes

See https://docs.google.com/document/d/1CqA8Ypji8FhiqZ5MujvilfV58btoNfIbsjK67qFtvd8/edit?usp=sharing for the spec.

The dialog does validate the values before generating a preview and saving. This wasn't tried on Windows so there may be bugs with the path validation that may need fixing.

The file format is PNG only at the moment

@timtmok timtmok requested review from jmcphers and softwarenerd April 1, 2024 15:46
@timtmok
Copy link
Contributor Author

timtmok commented Apr 1, 2024

Fixing this build failure. I messed up something that I didn't catch locally during code cleanup.

@@ -117,6 +125,7 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
<ActionBarButton iconId='positron-right-arrow' disabled={disableRight} tooltip={positronShowNextPlot} ariaLabel={positronShowNextPlot} onPressed={showNextPlotHandler} />

{enableZoomPlot && <ZoomPlotMenuButton actionHandler={zoomPlotHandler} zoomLevel={props.zoomLevel} />}
{enableSavingPlots && <ActionBarButton iconId='positron-save' tooltip={localize('positronSavePlot', "Save plot")} ariaLabel={localize('positronSavePlot', "Save plot")} onPressed={savePlotHandler} />}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit, for consistency, should localize('positronSavePlot', "Save plot") be saved in a variable, like positronClearAllPlots was on line 37?

Copy link
Contributor

Choose a reason for hiding this comment

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

We no longer need to preload all our calls to localize in modal dialogs. This was fixed in my modal dialog rework a couple of weeks ago.

So now, this code can be inlined:

/**
 * Localized strings.
 */
const positronShowPreviousPlot = localize('positronShowPreviousPlot', "Show previous plot");
const positronShowNextPlot = localize('positronShowNextPlot', "Show next plot");
const positronClearAllPlots = localize('positronClearAllPlots', "Clear all plots");

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's good to know. I thought it was for ease of re-use and clarity.

@petetronic
Copy link
Collaborator

Nice Tim! Do you want to log a follow-up task to handle if the target file already exists when saving?

@petetronic
Copy link
Collaborator

petetronic commented Apr 1, 2024

Also, for another follow up issue, do you want to add support for saving SVG based plots (presumably a rendering to PNG as an MVP)? Alternatively, if we don't support this in 1.0, the Save button should be disabled, perhaps with a tool-tip explaining the plot format is not supported for saving.

@@ -106,6 +111,9 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
const zoomPlotHandler = (zoomLevel: number) => {
props.zoomHandler(zoomLevel);
};
const savePlotHandler = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

One of these should be the implemetation:

	const savePlotHandler = async () => positronPlotsContext.positronPlotsService.savePlot();

	const savePlotHandler = async () => {
		await positronPlotsContext.positronPlotsService.savePlot();
	};

@@ -117,6 +125,7 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
<ActionBarButton iconId='positron-right-arrow' disabled={disableRight} tooltip={positronShowNextPlot} ariaLabel={positronShowNextPlot} onPressed={showNextPlotHandler} />

{enableZoomPlot && <ZoomPlotMenuButton actionHandler={zoomPlotHandler} zoomLevel={props.zoomLevel} />}
{enableSavingPlots && <ActionBarButton iconId='positron-save' tooltip={localize('positronSavePlot', "Save plot")} ariaLabel={localize('positronSavePlot', "Save plot")} onPressed={savePlotHandler} />}
Copy link
Contributor

Choose a reason for hiding this comment

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

We no longer need to preload all our calls to localize in modal dialogs. This was fixed in my modal dialog rework a couple of weeks ago.

So now, this code can be inlined:

/**
 * Localized strings.
 */
const positronShowPreviousPlot = localize('positronShowPreviousPlot', "Show previous plot");
const positronShowNextPlot = localize('positronShowNextPlot', "Show next plot");
const positronClearAllPlots = localize('positronClearAllPlots', "Clear all plots");


if (plot instanceof StaticPlotClient) {
// if it's a static plot, save the image to disk
const staticPlot = plot as StaticPlotClient;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unnecessary. plot is an instance of StaticPlotClient.

@timtmok
Copy link
Contributor Author

timtmok commented Apr 2, 2024

Nice Tim! Do you want to log a follow-up task to handle if the target file already exists when saving?

I think it's good now from using the fileDialogService.showSaveDialog(). It warns if that the file already exists. It's something that came for free. But it can be disabled there and re-implemented to be shown just before writing the file.

timtmok added 2 commits April 2, 2024 09:25
Fix focusable elements in modal
Inline string localization
Fix action bar button order and separator visibility
@timtmok
Copy link
Contributor Author

timtmok commented Apr 2, 2024

Also, for another follow up issue, do you want to add support for saving SVG based plots (presumably a rendering to PNG as an MVP)? Alternatively, if we don't support this in 1.0, the Save button should be disabled, perhaps with a tool-tip explaining the plot format is not supported for saving.

I think this can be added in without too much more work. Most of the existing code prior to this PR accounts for other formats.

@timtmok
Copy link
Contributor Author

timtmok commented Apr 2, 2024

I also fixed up the button order on the action bar. The save button wasn't in a consistent location due to the zoom button.

The tab order in the modal is also better now since the input number elements weren't being included properly in the focusable elements. There still seems to be a bug with the Save and Cancel buttons that don't seem to be included in the tab order. They should work as far as I can tell.

@timtmok
Copy link
Contributor Author

timtmok commented Apr 2, 2024

Fixed up the compile error. The change didn't get staged for commit for some reason.

@wesm wesm changed the title Plot saving Add UI modal dialog for saving plots Apr 2, 2024
Copy link
Collaborator

@jmcphers jmcphers left a comment

Choose a reason for hiding this comment

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

This could be merged as-is but I think we could make the error & plot state management better! Left a few notes.

@@ -299,13 +299,62 @@ export class PlotClientInstance extends Disposable implements IPositronPlotClien
return deferred.promise;
}

/**
* Requests that the plot be rendered at a specific size, but does not
* store the rendered plot to _lastRender. This is useful for previewing
Copy link
Collaborator

Choose a reason for hiding this comment

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

This duplicates a lot of the code in render(). Consider consolidating?

Copy link
Collaborator

Choose a reason for hiding this comment

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

One way to avoid this duplication would be to create a way to clone the plot client object so that you have two clients running against the same comm -- one for your preview window, another for the plots pane.

Since the backend is not stateful this is totally legit. It would also fix the other issue re: unnecessary re-rendering since the client for the preview window could also cache previous renders.

showSavePlotModalDialog(this._layoutService, this._keybindingService, this._fileDialogService, plot, this.savePlotAs, suggestedPath);
} else {
// if it's a webview plot, do nothing
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we show a warning in this case?

const acceptHandler = async () => {
if (validateInput()) {
setRendering(true);
const plotResult = await generatePreview();
Copy link
Collaborator

Choose a reason for hiding this comment

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

If an exception is thrown while generating the plot prior to saving it, we should show that error to the user in an error dialog.


const acceptHandler = async () => {
if (validateInput()) {
setRendering(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

In my testing, this caused the plot to be unnecessarily rendered a second time even though I'd already done a preview render at the correct size -- maybe we can avoid rendering in this case?

}
setRendering(true);
try {
const plotResult = await generatePreview();
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should catch errors thrown during preview and show them to the user -- probably in the error area already established.

(A very common error here would e.g. be the "figure margins too large" error you get in R when you try to draw a plot that's too small for its contents)

@timtmok
Copy link
Contributor Author

timtmok commented Apr 4, 2024

I've created a couple of issues for followup:

#2657
#2662

@timtmok timtmok merged commit 675b950 into main Apr 4, 2024
1 check passed
@timtmok timtmok deleted the plot-saving branch April 4, 2024 19:13
nstrayer pushed a commit that referenced this pull request Apr 17, 2024
Preview before saving dynamic plots
Use a progress bar when rendering
Save plot validation
Use save callback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants