Skip to content

Commit

Permalink
Add UI modal dialog for saving plots (#2589)
Browse files Browse the repository at this point in the history
Preview before saving dynamic plots
Use a progress bar when rendering
Save plot validation
Use save callback
  • Loading branch information
timtmok authored and nstrayer committed Apr 17, 2024
1 parent 6c83e1e commit 54b4130
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 32 deletions.
1 change: 1 addition & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@
"--vscode-positronModalDialog-checkboxBackground",
"--vscode-positronModalDialog-checkboxBorder",
"--vscode-positronModalDialog-checkboxForeground",
"--vscode-positronModalDialog-contrastBackground",
"--vscode-positronModalDialog-defaultButtonBackground",
"--vscode-positronModalDialog-defaultButtonForeground",
"--vscode-positronModalDialog-defaultButtonHoverBackground",
Expand Down
8 changes: 8 additions & 0 deletions src/vs/base/browser/ui/positronComponents/progressBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
*--------------------------------------------------------------------------------------------*/

.progress-bar-item {
height: 2px;
width: 100%;
}
16 changes: 16 additions & 0 deletions src/vs/base/browser/ui/positronComponents/progressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
*--------------------------------------------------------------------------------------------*/

import * as React from 'react';
import 'vs/css!./progressBar';

export interface ProgressBarProps {
value?: number;
}

export const ProgressBar = (props: ProgressBarProps) => {
return (
<progress className='progress-bar-item' value={props.value} />
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@
outline-offset: 2px;
outline: 1px solid var(--vscode-focusBorder) !important;
}

.positron-modal-dialog-box .labeled-folder-input span.error {
color: var(--vscode-errorForeground);
height: 1em;
display: block
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'vs/css!./labeledFolderInput';
// React.
import * as React from 'react';
import { ChangeEventHandler } from 'react'; // eslint-disable-line no-duplicate-imports
import { localize } from 'vs/nls';

// Other dependencies.
import { Button } from 'vs/base/browser/ui/positronComponents/button/button';
Expand All @@ -18,6 +19,10 @@ import { Button } from 'vs/base/browser/ui/positronComponents/button/button';
export interface LabeledFolderInputProps {
label: string;
value: string;
error?: boolean;
placeholder?: string;
readOnlyInput?: boolean;
inputRef?: React.RefObject<HTMLInputElement>;
onBrowse: VoidFunction;
onChange: ChangeEventHandler<HTMLInputElement>;
}
Expand All @@ -31,15 +36,18 @@ export const LabeledFolderInput = (props: LabeledFolderInputProps) => {
return (
<div className='labeled-folder-input'>
<label>
{props.label}:
{props.label}
<div className='folder-input'>
<input className='text-input' readOnly type='text' value={props.value} onChange={props.onChange} />
<input className='text-input' readOnly={props.readOnlyInput} placeholder={props.placeholder} type='text' value={props.value} onChange={props.onChange} />
<Button className='browse-button' onPressed={props.onBrowse}>
Browse...
{localize('positronFolderInputBrowse', 'Browse...')}
</Button>
</div>
</label>
</div>
);
};

LabeledFolderInput.defaultProps = {
readOnlyInput: true
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@
width: 100%;
}

.positron-modal-dialog-box .labeled-text-input label {
display: flex;
flex-direction: column;
}

.positron-modal-dialog-box .labeled-text-input .text-input {
margin-top: 4px;
}

.positron-modal-dialog-box .labeled-text-input span.error {
color: var(--vscode-errorForeground);
height: 1em;
display: block;
}

.positron-modal-dialog-box .labeled-text-input input.error {
border-color: var(--vscode-errorForeground);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import 'vs/css!./labeledTextInput';
// React.
import * as React from 'react';
import { ChangeEventHandler, forwardRef } from 'react'; // eslint-disable-line no-duplicate-imports
import { positronClassNames } from 'vs/base/common/positronUtilities';

/**
* LabeledTextInputProps interface.
*/
export interface LabeledTextInputProps {
label: string;
value: string;
value: string | number;
autoFocus?: boolean;
max?: number;
min?: number;
type?: 'text' | 'number';
error?: boolean;
onChange: ChangeEventHandler<HTMLInputElement>;
}

Expand All @@ -26,13 +31,17 @@ export const LabeledTextInput = forwardRef<HTMLInputElement, LabeledTextInputPro
// Render.
return (
<div className='labeled-text-input'>
<label>
{props.label}:
<input className='text-input' ref={ref} type='text' value={props.value} autoFocus={props.autoFocus} onChange={props.onChange} />
<label className='label'>
{props.label}
<input className={positronClassNames('text-input', { 'error': props.error })} ref={ref} type={props.type} value={props.value}
autoFocus={props.autoFocus} onChange={props.onChange} max={props.max} min={props.min} />
</label>
</div>
);
});

// Set the display name.
LabeledTextInput.displayName = 'LabeledTextInput';
LabeledTextInput.defaultProps = {
type: 'text'
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ import 'vs/css!./okCancelActionBar';
import * as React from 'react';

// Other dependencies.
import { ReactElement } from 'react'; // eslint-disable-line no-duplicate-imports
import { localize } from 'vs/nls';
import { Button } from 'vs/base/browser/ui/positronComponents/button/button';

/**
* OKCancelActionBarProps interface.
* @param okButtonTitle The title of the OK button.
* @param cancelButtonTitle The title of the Cancel button.
* @param preActions The pre-actions to render before the OK and cancel buttons.
* @param onAccept The function to call when the OK button is clicked.
* @param onCancel The function to call when the Cancel button is clicked.
*/
interface OKCancelActionBarProps {
okButtonTitle?: string;
cancelButtonTitle?: string;
preActions?: () => ReactElement;
onAccept: () => void;
onCancel: () => void;
}
Expand All @@ -28,9 +35,11 @@ interface OKCancelActionBarProps {
* @returns The rendered component.
*/
export const OKCancelActionBar = (props: OKCancelActionBarProps) => {
const preActions = props.preActions ? props.preActions() : null;
// Render.
return (
<div className='ok-cancel-action-bar top-separator'>
{preActions}
<Button className='action-bar-button default' onPressed={props.onAccept}>
{props.okButtonTitle ?? localize('positronOK', "OK")}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const focusableElementSelectors =
'button:not([disabled]),' +
'textarea:not([disabled]),' +
'input[type="text"]:not([disabled]),' +
'input[type="number"]:not([disabled]),' +
'input[type="radio"]:not([disabled]),' +
'input[type="checkbox"]:not([disabled]),' +
'select:not([disabled])';
Expand Down
8 changes: 8 additions & 0 deletions src/vs/workbench/common/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,14 @@ export const POSITRON_MODAL_DIALOG_FOREGROUND = registerColor('positronModalDial
hcLight: foreground
}, localize('positronModalDialog.foreground', "Positron modal dialog foreground color."));

// Positron modal dialog contrast background color.
export const POSITRON_MODAL_DIALOG_CONTRAST_BACKGROUND = registerColor('positronModalDialog.contrastBackground', {
dark: lighten(POSITRON_MODAL_DIALOG_BACKGROUND, 0.2),
light: darken(POSITRON_MODAL_DIALOG_BACKGROUND, 0.2),
hcDark: '#3a3d41',
hcLight: darken(POSITRON_MODAL_DIALOG_BACKGROUND, 0.2)
}, localize('positronModalDialog.contrastBackground', "Positron modal dialog contrast background color."));

// Positron modal dialog border color.
export const POSITRON_MODAL_DIALOG_BORDER = registerColor('positronModalDialog.border', {
dark: selectBorder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
const kPaddingLeft = 14;
const kPaddingRight = 8;

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

/**
* ActionBarsProps interface.
*/
Expand Down Expand Up @@ -67,16 +60,17 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
const disableLeft = noPlots || positronPlotsContext.selectedInstanceIndex <= 0;
const disableRight = noPlots || positronPlotsContext.selectedInstanceIndex >=
positronPlotsContext.positronPlotInstances.length - 1;
const selectedPlot = positronPlotsContext.positronPlotInstances[positronPlotsContext.selectedInstanceIndex];

// Only show the sizing policy controls when Positron is in control of the
// sizing (i.e. don't show it on static plots)
const enableSizingPolicy = hasPlots &&
positronPlotsContext.positronPlotInstances[positronPlotsContext.selectedInstanceIndex]
instanceof PlotClientInstance;

const enableZoomPlot = hasPlots &&
positronPlotsContext.positronPlotInstances[positronPlotsContext.selectedInstanceIndex]
instanceof StaticPlotClient;
const enableSizingPolicy = hasPlots
&& selectedPlot instanceof PlotClientInstance;
const enableZoomPlot = hasPlots
&& selectedPlot instanceof StaticPlotClient;
const enableSavingPlots = hasPlots
&& (selectedPlot instanceof PlotClientInstance
|| selectedPlot instanceof StaticPlotClient);

useEffect(() => {
// Empty for now.
Expand Down Expand Up @@ -106,18 +100,25 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
const zoomPlotHandler = (zoomLevel: number) => {
props.zoomHandler(zoomLevel);
};
const savePlotHandler = async () => {
positronPlotsContext.positronPlotsService.savePlot();
};

// Render.
return (
<PositronActionBarContextProvider {...props}>
<div className='action-bars'>
<PositronActionBar size='small' borderTop={true} borderBottom={true} paddingLeft={kPaddingLeft} paddingRight={kPaddingRight}>
<ActionBarRegion location='left'>
<ActionBarButton iconId='positron-left-arrow' disabled={disableLeft} tooltip={positronShowPreviousPlot} ariaLabel={positronShowPreviousPlot} onPressed={showPreviousPlotHandler} />
<ActionBarButton iconId='positron-right-arrow' disabled={disableRight} tooltip={positronShowNextPlot} ariaLabel={positronShowNextPlot} onPressed={showNextPlotHandler} />
<ActionBarButton iconId='positron-left-arrow' disabled={disableLeft} tooltip={localize('positronShowPreviousPlot', "Show previous plot")}
ariaLabel={localize('positronShowPreviousPlot', "Show previous plot")} onPressed={showPreviousPlotHandler} />
<ActionBarButton iconId='positron-right-arrow' disabled={disableRight} tooltip={localize('positronShowNextPlot', "Show next plot")}
ariaLabel={localize('positronShowNextPlot', "Show next plot")} onPressed={showNextPlotHandler} />

{(enableSizingPolicy || enableSavingPlots || enableZoomPlot) && <ActionBarSeparator />}
{enableSavingPlots && <ActionBarButton iconId='positron-save' tooltip={localize('positronSavePlot', "Save plot")}
ariaLabel={localize('positronSavePlot', "Save plot")} onPressed={savePlotHandler} />}
{enableZoomPlot && <ZoomPlotMenuButton actionHandler={zoomPlotHandler} zoomLevel={props.zoomLevel} />}
{enableSizingPolicy && <ActionBarSeparator />}
{enableSizingPolicy &&
<SizingPolicyMenuButton
keybindingService={props.keybindingService}
Expand All @@ -130,7 +131,8 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
<ActionBarRegion location='right'>
<HistoryPolicyMenuButton plotsService={positronPlotsContext.positronPlotsService} />
<ActionBarSeparator />
<ActionBarButton iconId='clear-all' align='right' disabled={noPlots} tooltip={positronClearAllPlots} ariaLabel={positronClearAllPlots} onPressed={clearAllPlotsHandler} />
<ActionBarButton iconId='clear-all' align='right' disabled={noPlots} tooltip={localize('positronClearAllPlots', "Clear all plots")}
ariaLabel={localize('positronClearAllPlots', "Clear all plots")} onPressed={clearAllPlotsHandler} />
</ActionBarRegion>
</PositronActionBar>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
*--------------------------------------------------------------------------------------------*/

.plot-preview-input {
height: 100%;
grid-template-rows: 1fr 2fr 4px 9fr;
grid-template-areas:
"browse"
"plot-input"
"preview-progress"
"preview";
grid-gap: 10px;
display: grid;
}

.plot-preview-input .plot-input {
grid-area: plot-input;
display: grid;
grid-template-columns: repeat(3, auto);
grid-template-areas:
"input input input"
"error error error";
justify-content: space-between;
}

.plot-input input {
grid-area: input;
}

.plot-input div.error {
padding-top: 4px;
grid-column: 1 / span 3;
display: flex;
flex-direction: column;
color: var(--vscode-errorForeground);
}

.plot-preview-input .labeled-text-input {
width: auto;
}

.plot-preview-input .labeled-text-input input {
width: 100px;
}

.plot-preview-input .preview-progress {
grid-area: preview-progress;
display: flex;
}

.plot-preview-container {
height: 100%;
columns: 2;
display: flex;
flex-direction: column;
row-gap: 10px;
}

img.plot-preview {
max-height: 100%;
max-width: 100%;
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-style: solid;
border-color: var(--vscode-positronModalDialog-contrastBackground);
border-width: thin;
}

.plot-save-dialog-action-bar {
display: flex;
position: absolute;
justify-content: space-between;
bottom: 0;
left: 0;
right: 0;
height: 64px;
gap: 10px;
margin: 0 16px;
}

.plot-preview-image-container {
overflow: hidden;
padding: 2px;
grid-area: preview;
}

.plot-save-dialog-action-bar div.ok-cancel-action-bar {
justify-content: end;
}

.plot-save-dialog-action-bar .button.action-bar-button {
padding: 10px;
}
Loading

0 comments on commit 54b4130

Please sign in to comment.