Skip to content

Commit

Permalink
Copy plot to clipboard (#2640)
Browse files Browse the repository at this point in the history
Implement write image in clipboard service
Support dynamic plots
Only show copy action for supported plots
Add toast notification
Catch error writing plot to clipboard
  • Loading branch information
timtmok authored Apr 8, 2024
1 parent b28c219 commit 08917dd
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 1 deletion.
11 changes: 11 additions & 0 deletions src/vs/platform/clipboard/browser/clipboardService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,17 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer
return this.resources;
}

// --- Start Positron ---
async writeImage(data: string): Promise<void> {
const blob = new Blob([data], { type: 'image/png' });
navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
}
)]);
}
// --- End Positron ---

private async computeResourcesStateHash(): Promise<number | undefined> {
if (this.resources.length === 0) {
return undefined; // no resources, no hash needed
Expand Down
4 changes: 4 additions & 0 deletions src/vs/platform/clipboard/common/clipboardService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export interface IClipboardService {
*/
writeResources(resources: URI[]): Promise<void>;

// --- Start Positron ---
writeImage(data: string): Promise<void>;
// --- End Positron ---

/**
* Reads resources from the system clipboard.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/vs/platform/clipboard/test/common/testClipboardService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export class TestClipboardService implements IClipboardService {
this.resources = resources;
}

// --- Start Positron ---
async writeImage(data: string): Promise<void> {
// no-op
}
// --- End Positron ---

async readResources(): Promise<URI[]> {
return this.resources ?? [];
}
Expand Down
4 changes: 4 additions & 0 deletions src/vs/platform/native/common/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export interface ICommonNativeHostService {
readClipboardBuffer(format: string): Promise<VSBuffer>;
hasClipboard(format: string, type?: 'selection' | 'clipboard'): Promise<boolean>;

// --- Start Positron ---
writeClipboardImage(dataUri: string): Promise<void>;
// --- End Positron ---

// macOS Touchbar
newWindowTab(): Promise<void>;
showPreviousWindowTab(): Promise<void>;
Expand Down
11 changes: 11 additions & 0 deletions src/vs/platform/native/electron-main/nativeHostMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electr
import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow';
import { CancellationError } from 'vs/base/common/errors';

// --- Start Positron ---
// eslint-disable-next-line no-duplicate-imports
import { nativeImage } from 'electron';
// --- End Positron ---

export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }

export const INativeHostMainService = createDecorator<INativeHostMainService>('nativeHostMainService');
Expand Down Expand Up @@ -647,6 +652,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return clipboard.has(format, type);
}

// --- Start Positron ---
async writeClipboardImage(windowId: number | undefined, dataUri: string): Promise<void> {
return clipboard.writeImage(nativeImage.createFromDataURL(dataUri));
}
// --- End Positron ---

//#endregion


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
&& (selectedPlot instanceof PlotClientInstance
|| selectedPlot instanceof StaticPlotClient);

const enableCopyPlot = hasPlots &&
(positronPlotsContext.positronPlotInstances[positronPlotsContext.selectedInstanceIndex]
instanceof StaticPlotClient
|| positronPlotsContext.positronPlotInstances[positronPlotsContext.selectedInstanceIndex]
instanceof PlotClientInstance);

useEffect(() => {
// Empty for now.
});
Expand Down Expand Up @@ -104,6 +110,17 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
positronPlotsContext.positronPlotsService.savePlot();
};

const copyPlotHandler = () => {
positronPlotsContext.positronPlotsService.copyPlotToClipboard()
.then(() => {
positronPlotsContext.notificationService.info(localize('positronPlotsServiceCopyToClipboard', 'Plot copied to clipboard'));
})
.catch((error) => {
positronPlotsContext.notificationService.error(localize('positronPlotsServiceCopyToClipboardError', 'Failed to copy plot to clipboard: {0}', error.message));
});

};

// Render.
return (
<PositronActionBarContextProvider {...props}>
Expand All @@ -118,6 +135,8 @@ export const ActionBars = (props: PropsWithChildren<ActionBarsProps>) => {
{(enableSizingPolicy || enableSavingPlots || enableZoomPlot) && <ActionBarSeparator />}
{enableSavingPlots && <ActionBarButton iconId='positron-save' tooltip={localize('positronSavePlot', "Save plot")}
ariaLabel={localize('positronSavePlot', "Save plot")} onPressed={savePlotHandler} />}
{enableCopyPlot && <ActionBarButton iconId='copy' disabled={!hasPlots} tooltip={localize('positron-copy-plot', "Copy plot to clipboard")} ariaLabel={localize('positron-copy-plot', "Copy plot to clipboard")}
onPressed={copyPlotHandler} />}
{enableZoomPlot && <ZoomPlotMenuButton actionHandler={zoomPlotHandler} zoomLevel={props.zoomLevel} />}
{enableSizingPolicy &&
<SizingPolicyMenuButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la
import { URI } from 'vs/base/common/uri';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';

/** The maximum number of recent executions to store. */
const MaxRecentExecutions = 10;
Expand Down Expand Up @@ -107,7 +108,8 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
@IKeybindingService private readonly _keybindingService: IKeybindingService) {
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IClipboardService private _clipboardService: IClipboardService) {
super();

// Register for language runtime service startups
Expand Down Expand Up @@ -745,6 +747,25 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe
});
}

async copyPlotToClipboard(): Promise<void> {
const plot = this._plots.find(plot => plot.id === this.selectedPlotId);
if (plot instanceof StaticPlotClient) {
try {
await this._clipboardService.writeImage(plot.uri);
} catch (error) {
throw new Error(error.message);
}
} else if (plot instanceof PlotClientInstance) {
if (plot.lastRender?.uri) {
try {
await this._clipboardService.writeImage(plot.lastRender.uri);
} catch (error) {
throw new Error(error.message);
}
}
}
}

/**
* Generates a storage key for a plot.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export class NativeClipboardService implements IClipboardService {
}
}

// --- Start Positron ---
async writeImage(data: string): Promise<void> {
return this.nativeHostService.writeClipboardImage(data);
}
// --- End Positron ---

async readResources(): Promise<URI[]> {
return this.bufferToResources(await this.nativeHostService.readClipboardBuffer(NativeClipboardService.FILE_FORMAT));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ export interface IPositronPlotsService {
*/
selectHistoryPolicy(policy: HistoryPolicy): void;

/**
* Copies the selected plot to the clipboard.
*
* @throws An error if the plot cannot be copied.
*/
copyPlotToClipboard(): Promise<void>;

/**
* Saves the plot.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export class TestNativeHostService implements INativeHostService {
async hasClipboard(format: string, type?: 'selection' | 'clipboard' | undefined): Promise<boolean> { return false; }
async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined> { return undefined; }
async profileRenderer(): Promise<any> { throw new Error(); }

// --- Start Positron ---
async writeClipboardImage(dataUri: string): Promise<void> { }
// --- End Positron ---
}

export class TestExtensionTipsService extends AbstractNativeExtensionTipsService {
Expand Down

0 comments on commit 08917dd

Please sign in to comment.