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
exports[`<AnnotateArea/> should render correctly 1`] = `
<svg
data-testid="annotate-area"
height={800}
id="annotate-area"
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}
onMouseUp={[Function]}
style={
<AnnotateArea
annotateAreaDimensions={
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
className="SnippetImage"
height={800}
id="screenSnippet"
width={800}
x={0}
xlinkHref="very-nice-path"
y={0}
/>
</svg>
<div
id="annotate-wrapper"
style={
Object {
"height": 800,
"width": 800,
}
}
>
<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"
>
<AnnotateArea
annotateAreaDimensions={
Object {
"height": 600,
"width": 800,
}
}
backgroundImagePath=""
chosenTool="PEN"
data-testid="annotate-component"
highlightColor="rgba(0, 142, 255, 0.64)"
imageDimensions={
Object {
"height": 600,
"width": 800,
highlightColor="rgba(0, 142, 255, 0.64)"
imageDimensions={
Object {
"height": 600,
"width": 800,
}
}
}
onChange={[Function]}
paths={Array []}
penColor="rgba(0, 142, 255, 1)"
screenSnippetPath="Screen-Snippet"
/></div>
onChange={[Function]}
paths={Array []}
penColor="rgba(0, 142, 255, 1)"
/>
</div>
</main>
<footer>
<button

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { LazyBrush } from 'lazy-brush';
import * as React from 'react';
import { IImageDimensions, IPath, IPoint, Tool } from './snipping-tool';
import { IDimensions, IPath, IPoint, Tool } from './snipping-tool';
const { useState } = React;
@ -17,9 +17,10 @@ export interface IAnnotateAreaProps {
highlightColor: string;
penColor: string;
onChange: (paths: IPath[]) => void;
imageDimensions: IImageDimensions;
imageDimensions: IDimensions;
annotateAreaDimensions: IDimensions;
chosenTool: Tool;
screenSnippetPath: string;
backgroundImagePath?: string;
}
const lazy = new LazyBrush({
@ -27,9 +28,6 @@ const lazy = new LazyBrush({
enabled: true,
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 HIGHLIGHT_WIDTH = 28;
@ -40,7 +38,8 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
onChange,
imageDimensions,
chosenTool,
screenSnippetPath,
backgroundImagePath,
annotateAreaDimensions,
}) => {
const [isDrawing, setIsDrawing] = useState(false);
@ -66,10 +65,19 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
// Utility functions
const getMousePosition = (e: React.MouseEvent) => {
// We need to offset for elements in the window that is not the annotate area
const x = imageDimensions.width >= MIN_ANNOTATE_AREA_WIDTH ? e.pageX : e.pageX - (MIN_ANNOTATE_AREA_WIDTH - imageDimensions.width) / 2;
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);
return { x, y };
const target = document.getElementById('annotate-area');
if (target) {
const rect = target.getBoundingClientRect();
// 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
@ -113,12 +121,12 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
const addPathPoint = (e: React.MouseEvent) => {
const p = [...paths];
const mousePos: IPoint = getMousePosition(e);
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
if (chosenTool === Tool.highlight) {
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
onChange(addHighlightPoint(p, point));
} else {
onChange(addPenPoint(p, mousePos));
onChange(addPenPoint(p, point));
}
if (!isDrawing) {
setIsDrawing(true);
@ -216,29 +224,46 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
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 (
<svg
data-testid='annotate-area'
style={{ cursor: 'crosshair' }}
id='annotate-area'
width={imageDimensions.width}
height={imageDimensions.height}
onMouseDown={handleMouseDown}
onMouseUp={stopDrawing}
onMouseMove={handleMouseMove}
onMouseLeave={stopDrawing}
>
<image
x={0}
y={0}
id='screenSnippet'
xlinkHref={screenSnippetPath}
<div
id='annotate-wrapper'
style={getAnnotateWrapperStyle()}>
<svg
data-testid='annotate-area'
style={{ cursor: 'crosshair' }}
id='annotate-area'
width={imageDimensions.width}
height={imageDimensions.height}
className='SnippetImage'
/>
{renderPaths(getSvgPathsData(paths))}
</svg>
onMouseDown={handleMouseDown}
onMouseUp={stopDrawing}
onMouseMove={handleMouseMove}
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 ColorPickerPill, { IColor } from './color-picker-pill';
const { useState, useRef, useEffect } = React;
const { useState, useRef, useEffect, useLayoutEffect } = React;
export enum Tool {
pen = 'PEN',
@ -25,15 +25,7 @@ export interface IPoint {
y: number;
}
export interface ISvgPath {
svgPath: string;
key: string;
strokeWidth: number;
color: string;
shouldShow: boolean;
}
export interface IImageDimensions {
export interface IDimensions {
width: number;
height: number;
}
@ -60,11 +52,15 @@ const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPaths }) => {
// State preparation functions
const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet');
const [screenSnippetPath, setScreenSnippetPath] = useState('');
const [imageDimensions, setImageDimensions] = useState({
height: 600,
width: 800,
});
const [annotateAreaDimensions, setAnnotateAreaDimensions] = useState({
height: 600,
width: 800,
});
const [paths, setPaths] = useState<IPath[]>(existingPaths || []);
const [chosenTool, setChosenTool] = useState(Tool.pen);
const [penColor, setPenColor] = useState<IColor>({ rgbaColor: 'rgba(0, 142, 255, 1)' });
@ -79,12 +75,24 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
false,
);
const getSnipImageData = ({ }, { snipImage, height, width }) => {
const getSnipImageData = ({ }, {
snipImage,
annotateAreaHeight,
annotateAreaWidth,
snippetImageHeight,
snippetImageWidth,
}) => {
setScreenSnippetPath(snipImage);
setImageDimensions({ height, width });
setImageDimensions({ height: snippetImageHeight, width: snippetImageWidth });
setAnnotateAreaDimensions({ height: annotateAreaHeight, width: annotateAreaWidth });
};
useLayoutEffect(() => {
ipcRenderer.once('snipping-tool-data', getSnipImageData);
return () => {
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
};
}, []);
// Hook that alerts clicks outside of the passed refs
const useClickOutsideExaminer = (
@ -179,12 +187,12 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
'src',
'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) => {
// Listens to when the img is loaded in memory and adds the data from the SVG paths + screenSnippet to the canvas
img.onload = () => {
ctx.drawImage(screenSnippet, 0, 0);
ctx.drawImage(backgroundImage, 0, 0);
ctx.drawImage(img, 0, 0);
try {
// Extracts base 64 png img data from the canvas
@ -310,8 +318,9 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
penColor={penColor.rgbaColor}
onChange={setPaths}
imageDimensions={imageDimensions}
screenSnippetPath={screenSnippetPath}
backgroundImagePath={screenSnippetPath}
chosenTool={chosenTool}
annotateAreaDimensions={annotateAreaDimensions}
/>
</div>
</main>

View File

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