feat: SDA-2705 Add possibility for panning

This commit is contained in:
psjostrom 2020-11-26 10:27:18 +01:00
parent 94eb41719b
commit e18b431c98
8 changed files with 184 additions and 115 deletions

View File

@ -1,29 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AnnotateArea/> should render correctly 1`] = ` exports[`<AnnotateArea/> should render correctly 1`] = `
<svg <AnnotateArea
data-testid="annotate-area" annotateAreaDimensions={
height={800}
id="annotate-area"
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}
onMouseUp={[Function]}
style={
Object { Object {
"cursor": "crosshair", "height": 800,
"width": 800,
} }
} }
width={800} chosenTool="PEN"
highlightColor="rgba(233, 0, 0, 0.64)"
imageDimensions={
Object {
"height": 800,
"width": 800,
}
}
onChange={[MockFunction]}
paths={Array []}
penColor="rgba(38, 196, 58, 1)"
screenSnippetPath="very-nice-path"
> >
<image <div
className="SnippetImage" id="annotate-wrapper"
height={800} style={
id="screenSnippet" Object {
width={800} "height": 800,
x={0} "width": 800,
xlinkHref="very-nice-path" }
y={0} }
/> >
</svg> <svg
data-testid="annotate-area"
height={800}
id="annotate-area"
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}
onMouseUp={[Function]}
style={
Object {
"cursor": "crosshair",
}
}
width={800}
/>
</div>
</AnnotateArea>
`; `;

View File

@ -62,20 +62,27 @@ exports[`Snipping Tool should render correctly 1`] = `
className="imageContainer" className="imageContainer"
> >
<AnnotateArea <AnnotateArea
annotateAreaDimensions={
Object {
"height": 600,
"width": 800,
}
}
backgroundImagePath=""
chosenTool="PEN" chosenTool="PEN"
data-testid="annotate-component" data-testid="annotate-component"
highlightColor="rgba(0, 142, 255, 0.64)" highlightColor="rgba(0, 142, 255, 0.64)"
imageDimensions={ imageDimensions={
Object { Object {
"height": 600, "height": 600,
"width": 800, "width": 800,
}
} }
} onChange={[Function]}
onChange={[Function]} paths={Array []}
paths={Array []} penColor="rgba(0, 142, 255, 1)"
penColor="rgba(0, 142, 255, 1)" />
screenSnippetPath="Screen-Snippet" </div>
/></div>
</main> </main>
<footer> <footer>
<button <button

View File

@ -1,4 +1,4 @@
import { shallow } from 'enzyme'; import { mount } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import AnnotateArea from '../src/renderer/components/annotate-area'; import AnnotateArea from '../src/renderer/components/annotate-area';
import { Tool } from '../src/renderer/components/snipping-tool'; import { Tool } from '../src/renderer/components/snipping-tool';
@ -9,6 +9,7 @@ const defaultProps = {
penColor: 'rgba(38, 196, 58, 1)', penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(), onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 }, imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.pen, chosenTool: Tool.pen,
screenSnippetPath: 'very-nice-path', screenSnippetPath: 'very-nice-path',
}; };
@ -19,25 +20,27 @@ afterEach(() => {
describe('<AnnotateArea/>', () => { describe('<AnnotateArea/>', () => {
it('should render correctly', () => { it('should render correctly', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />); const wrapper = mount(<AnnotateArea {...defaultProps} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('should call onChange when drawn on annotate area', () => { it('should call onChange when drawn on annotate area', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />); const wrapper = mount(<AnnotateArea {...defaultProps} />);
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 }); const area = wrapper.find('[data-testid="annotate-area"]');
wrapper.simulate('mouseup'); area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1); expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
}); });
it('should call onChange with correct pen props if drawn drawn on annotate area with pen', () => { it('should call onChange with correct pen props if drawn drawn on annotate area with pen', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />); const wrapper = mount(<AnnotateArea {...defaultProps} />);
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 }); const area = wrapper.find('[data-testid="annotate-area"]');
wrapper.simulate('mouseup'); area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledWith([{ expect(defaultProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(38, 196, 58, 1)', color: 'rgba(38, 196, 58, 1)',
key: 'path0', key: 'path0',
points: [{ x: 2, y: 1 }], points: [{ x: 0, y: 0 }],
shouldShow: true, shouldShow: true,
strokeWidth: 5, strokeWidth: 5,
}]); }]);
@ -50,12 +53,14 @@ describe('<AnnotateArea/>', () => {
penColor: 'rgba(38, 196, 58, 1)', penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(), onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 }, imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight, chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path', screenSnippetPath: 'very-nice-path',
}; };
const wrapper = shallow(<AnnotateArea {...highlightProps} />); const wrapper = mount(<AnnotateArea {...highlightProps} />);
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 }); const area = wrapper.find('[data-testid="annotate-area"]');
wrapper.simulate('mouseup'); area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(highlightProps.onChange).toHaveBeenCalledWith([{ expect(highlightProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(233, 0, 0, 0.64)', color: 'rgba(233, 0, 0, 0.64)',
key: 'path0', key: 'path0',
@ -78,15 +83,16 @@ describe('<AnnotateArea/>', () => {
penColor: 'rgba(38, 196, 58, 1)', penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(), onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 }, imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight, chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path', screenSnippetPath: 'very-nice-path',
}; };
const wrapper = shallow(<AnnotateArea {...pathProps} />); const wrapper = mount(<AnnotateArea {...pathProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(true); expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(true);
}); });
it('should not render any path if no path is provided in props', () => { it('should not render any path if no path is provided in props', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />); const wrapper = mount(<AnnotateArea {...defaultProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false); expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
}); });
@ -103,10 +109,11 @@ describe('<AnnotateArea/>', () => {
penColor: 'rgba(38, 196, 58, 1)', penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(), onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 }, imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser, chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path', screenSnippetPath: 'very-nice-path',
}; };
const wrapper = shallow(<AnnotateArea {...pathProps} />); const wrapper = mount(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]'); const path = wrapper.find('[data-testid="path0"]');
path.simulate('click'); path.simulate('click');
expect(pathProps.onChange).toHaveBeenCalledWith([{ expect(pathProps.onChange).toHaveBeenCalledWith([{

View File

@ -13,7 +13,7 @@ describe('Snipping Tool', () => {
it('should set up a "once" listener for snipping-tool-data event on mounting', () => { it('should set up a "once" listener for snipping-tool-data event on mounting', () => {
const spy = jest.spyOn(ipcRenderer, 'once'); const spy = jest.spyOn(ipcRenderer, 'once');
shallow(React.createElement(SnippingTool)); mount(React.createElement(SnippingTool));
expect(spy).toBeCalledWith('snipping-tool-data', expect.any(Function)); expect(spy).toBeCalledWith('snipping-tool-data', expect.any(Function));
}); });
@ -61,7 +61,7 @@ describe('Snipping Tool', () => {
await waitForPromisesToResolve(); await waitForPromisesToResolve();
expect(spy).toBeCalledWith('upload-snippet', { expect(spy).toBeCalledWith('upload-snippet', {
base64PngData: 'NO CANVAS', base64PngData: 'NO CANVAS',
screenSnippetPath: 'Screen-Snippet', screenSnippetPath: '',
}); });
}); });
}); });

View File

@ -963,8 +963,10 @@ export class WindowHandler {
const MIN_WIDTH = 320; const MIN_WIDTH = 320;
const CONTAINER_HEIGHT = 175; const CONTAINER_HEIGHT = 175;
const OS_PADDING = 25; const OS_PADDING = 25;
let height: number = dimensions?.height || 0; const snippetImageHeight = dimensions?.height || 0;
let width: number = dimensions?.width || 0; const snippetImageWidth = dimensions?.width || 0;
let annotateAreaHeight = snippetImageHeight;
let annotateAreaWidth = snippetImageWidth;
if (parentWindow) { if (parentWindow) {
const { bounds: { height: sHeight, width: sWidth } } = electron.screen.getDisplayMatching(parentWindow.getBounds()); const { bounds: { height: sHeight, width: sWidth } } = electron.screen.getDisplayMatching(parentWindow.getBounds());
@ -972,24 +974,24 @@ export class WindowHandler {
// This calculation is to make sure the // This calculation is to make sure the
// snippet window does not cover the entire screen // snippet window does not cover the entire screen
const maxScreenHeight: number = calculatePercentage(sHeight, 90); const maxScreenHeight: number = calculatePercentage(sHeight, 90);
if (height > maxScreenHeight) { if (annotateAreaHeight > maxScreenHeight) {
height = maxScreenHeight; annotateAreaHeight = maxScreenHeight;
} }
const maxScreenWidth: number = calculatePercentage(sWidth, 90); const maxScreenWidth: number = calculatePercentage(sWidth, 90);
if (width > maxScreenWidth) { if (annotateAreaWidth > maxScreenWidth) {
width = maxScreenWidth; annotateAreaWidth = maxScreenWidth;
} }
// decrease image height when there is no space for the container window // decrease image height when there is no space for the container window
if ((sHeight - height) < CONTAINER_HEIGHT) { if ((sHeight - annotateAreaHeight) < CONTAINER_HEIGHT) {
height -= CONTAINER_HEIGHT; annotateAreaHeight -= CONTAINER_HEIGHT;
} }
} }
const windowHeight = height + CONTAINER_HEIGHT - OS_PADDING; const windowHeight = annotateAreaHeight + CONTAINER_HEIGHT - OS_PADDING;
const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts( const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts(
{ {
width, width: annotateAreaWidth,
height: windowHeight, height: windowHeight,
minHeight: MIN_HEIGHT, minHeight: MIN_HEIGHT,
minWidth: MIN_WIDTH, minWidth: MIN_WIDTH,
@ -1022,8 +1024,10 @@ export class WindowHandler {
this.snippingToolWindow.webContents.once('did-finish-load', async () => { this.snippingToolWindow.webContents.once('did-finish-load', async () => {
const snippingToolInfo = { const snippingToolInfo = {
snipImage, snipImage,
height, annotateAreaHeight,
width, annotateAreaWidth,
snippetImageHeight,
snippetImageWidth,
}; };
if (this.snippingToolWindow && windowExists(this.snippingToolWindow)) { if (this.snippingToolWindow && windowExists(this.snippingToolWindow)) {
this.snippingToolWindow.webContents.send( this.snippingToolWindow.webContents.send(

View File

@ -1,6 +1,6 @@
import { LazyBrush } from 'lazy-brush'; import { LazyBrush } from 'lazy-brush';
import * as React from 'react'; import * as React from 'react';
import { IImageDimensions, IPath, IPoint, Tool } from './snipping-tool'; import { IDimensions, IPath, IPoint, Tool } from './snipping-tool';
const { useState } = React; const { useState } = React;
@ -17,9 +17,10 @@ export interface IAnnotateAreaProps {
highlightColor: string; highlightColor: string;
penColor: string; penColor: string;
onChange: (paths: IPath[]) => void; onChange: (paths: IPath[]) => void;
imageDimensions: IImageDimensions; imageDimensions: IDimensions;
annotateAreaDimensions: IDimensions;
chosenTool: Tool; chosenTool: Tool;
screenSnippetPath: string; backgroundImagePath?: string;
} }
const lazy = new LazyBrush({ const lazy = new LazyBrush({
@ -27,9 +28,6 @@ const lazy = new LazyBrush({
enabled: true, enabled: true,
initialPoint: { x: 0, y: 0 }, initialPoint: { x: 0, y: 0 },
}); });
const TOP_MENU_HEIGHT = 48;
const MIN_ANNOTATE_AREA_HEIGHT = 200;
const MIN_ANNOTATE_AREA_WIDTH = 312;
const PEN_WIDTH = 5; const PEN_WIDTH = 5;
const HIGHLIGHT_WIDTH = 28; const HIGHLIGHT_WIDTH = 28;
@ -40,7 +38,8 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
onChange, onChange,
imageDimensions, imageDimensions,
chosenTool, chosenTool,
screenSnippetPath, backgroundImagePath,
annotateAreaDimensions,
}) => { }) => {
const [isDrawing, setIsDrawing] = useState(false); const [isDrawing, setIsDrawing] = useState(false);
@ -66,10 +65,19 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
// Utility functions // Utility functions
const getMousePosition = (e: React.MouseEvent) => { const getMousePosition = (e: React.MouseEvent) => {
// We need to offset for elements in the window that is not the annotate area const target = document.getElementById('annotate-area');
const x = imageDimensions.width >= MIN_ANNOTATE_AREA_WIDTH ? e.pageX : e.pageX - (MIN_ANNOTATE_AREA_WIDTH - imageDimensions.width) / 2; if (target) {
const y = imageDimensions.height >= MIN_ANNOTATE_AREA_HEIGHT ? (e.pageY - TOP_MENU_HEIGHT) : (e.pageY - ((MIN_ANNOTATE_AREA_HEIGHT - imageDimensions.height) / 2) - TOP_MENU_HEIGHT); const rect = target.getBoundingClientRect();
return { x, y }; // Offseting the scrolled X and Y inside the annotate area
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
}
return {
x: e.clientX,
y: e.clientY,
};
}; };
// Render and preparing render functions // Render and preparing render functions
@ -113,12 +121,12 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
const addPathPoint = (e: React.MouseEvent) => { const addPathPoint = (e: React.MouseEvent) => {
const p = [...paths]; const p = [...paths];
const mousePos: IPoint = getMousePosition(e); const mousePos: IPoint = getMousePosition(e);
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
if (chosenTool === Tool.highlight) { if (chosenTool === Tool.highlight) {
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
onChange(addHighlightPoint(p, point)); onChange(addHighlightPoint(p, point));
} else { } else {
onChange(addPenPoint(p, mousePos)); onChange(addPenPoint(p, point));
} }
if (!isDrawing) { if (!isDrawing) {
setIsDrawing(true); setIsDrawing(true);
@ -216,29 +224,46 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
addPathPoint(e); addPathPoint(e);
}; };
const getAnnotateWrapperStyle = () => {
const shouldShowScrollBars =
imageDimensions.height > annotateAreaDimensions.height ||
imageDimensions.width > annotateAreaDimensions.width;
return {
width: annotateAreaDimensions.width,
height: annotateAreaDimensions.height,
...(shouldShowScrollBars && { overflow: 'scroll' }),
};
};
return ( return (
<svg <div
data-testid='annotate-area' id='annotate-wrapper'
style={{ cursor: 'crosshair' }} style={getAnnotateWrapperStyle()}>
id='annotate-area' <svg
width={imageDimensions.width} data-testid='annotate-area'
height={imageDimensions.height} style={{ cursor: 'crosshair' }}
onMouseDown={handleMouseDown} id='annotate-area'
onMouseUp={stopDrawing}
onMouseMove={handleMouseMove}
onMouseLeave={stopDrawing}
>
<image
x={0}
y={0}
id='screenSnippet'
xlinkHref={screenSnippetPath}
width={imageDimensions.width} width={imageDimensions.width}
height={imageDimensions.height} height={imageDimensions.height}
className='SnippetImage' onMouseDown={handleMouseDown}
/> onMouseUp={stopDrawing}
{renderPaths(getSvgPathsData(paths))} onMouseMove={handleMouseMove}
</svg> onMouseLeave={stopDrawing}
>
{
backgroundImagePath &&
<image
x={0}
y={0}
id='backgroundImage'
xlinkHref={backgroundImagePath}
width={imageDimensions.width}
height={imageDimensions.height}
/>}
{renderPaths(getSvgPathsData(paths))}
</svg>
</div>
); );
}; };

View File

@ -4,7 +4,7 @@ import { i18n } from '../../common/i18n-preload';
import AnnotateArea from './annotate-area'; import AnnotateArea from './annotate-area';
import ColorPickerPill, { IColor } from './color-picker-pill'; import ColorPickerPill, { IColor } from './color-picker-pill';
const { useState, useRef, useEffect } = React; const { useState, useRef, useEffect, useLayoutEffect } = React;
export enum Tool { export enum Tool {
pen = 'PEN', pen = 'PEN',
@ -25,15 +25,7 @@ export interface IPoint {
y: number; y: number;
} }
export interface ISvgPath { export interface IDimensions {
svgPath: string;
key: string;
strokeWidth: number;
color: string;
shouldShow: boolean;
}
export interface IImageDimensions {
width: number; width: number;
height: number; height: number;
} }
@ -60,11 +52,15 @@ const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPaths }) => { const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPaths }) => {
// State preparation functions // State preparation functions
const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet'); const [screenSnippetPath, setScreenSnippetPath] = useState('');
const [imageDimensions, setImageDimensions] = useState({ const [imageDimensions, setImageDimensions] = useState({
height: 600, height: 600,
width: 800, width: 800,
}); });
const [annotateAreaDimensions, setAnnotateAreaDimensions] = useState({
height: 600,
width: 800,
});
const [paths, setPaths] = useState<IPath[]>(existingPaths || []); const [paths, setPaths] = useState<IPath[]>(existingPaths || []);
const [chosenTool, setChosenTool] = useState(Tool.pen); const [chosenTool, setChosenTool] = useState(Tool.pen);
const [penColor, setPenColor] = useState<IColor>({ rgbaColor: 'rgba(0, 142, 255, 1)' }); const [penColor, setPenColor] = useState<IColor>({ rgbaColor: 'rgba(0, 142, 255, 1)' });
@ -79,12 +75,24 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
false, false,
); );
const getSnipImageData = ({ }, { snipImage, height, width }) => { const getSnipImageData = ({ }, {
snipImage,
annotateAreaHeight,
annotateAreaWidth,
snippetImageHeight,
snippetImageWidth,
}) => {
setScreenSnippetPath(snipImage); setScreenSnippetPath(snipImage);
setImageDimensions({ height, width }); setImageDimensions({ height: snippetImageHeight, width: snippetImageWidth });
setAnnotateAreaDimensions({ height: annotateAreaHeight, width: annotateAreaWidth });
}; };
useLayoutEffect(() => {
ipcRenderer.once('snipping-tool-data', getSnipImageData); ipcRenderer.once('snipping-tool-data', getSnipImageData);
return () => {
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
};
}, []);
// Hook that alerts clicks outside of the passed refs // Hook that alerts clicks outside of the passed refs
const useClickOutsideExaminer = ( const useClickOutsideExaminer = (
@ -179,12 +187,12 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
'src', 'src',
'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))), 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))),
); );
const screenSnippet = document.getElementById('screenSnippet') as HTMLImageElement; const backgroundImage = document.getElementById('backgroundImage') as HTMLImageElement;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Listens to when the img is loaded in memory and adds the data from the SVG paths + screenSnippet to the canvas // Listens to when the img is loaded in memory and adds the data from the SVG paths + screenSnippet to the canvas
img.onload = () => { img.onload = () => {
ctx.drawImage(screenSnippet, 0, 0); ctx.drawImage(backgroundImage, 0, 0);
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
try { try {
// Extracts base 64 png img data from the canvas // Extracts base 64 png img data from the canvas
@ -310,8 +318,9 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
penColor={penColor.rgbaColor} penColor={penColor.rgbaColor}
onChange={setPaths} onChange={setPaths}
imageDimensions={imageDimensions} imageDimensions={imageDimensions}
screenSnippetPath={screenSnippetPath} backgroundImagePath={screenSnippetPath}
chosenTool={chosenTool} chosenTool={chosenTool}
annotateAreaDimensions={annotateAreaDimensions}
/> />
</div> </div>
</main> </main>

View File

@ -4,10 +4,6 @@
@version-text-color: rgb(47, 47, 47, 1); @version-text-color: rgb(47, 47, 47, 1);
@text-padding: 10px; @text-padding: 10px;
::-webkit-scrollbar {
display: none;
}
body { body {
background-color: white; background-color: white;
margin: 0; margin: 0;