Merge pull request #1107 from psjostrom/SDA-2402-Annotate-Screenshot-Tool

feat: SDA-2532 Work on state for annotate and adding color picker
This commit is contained in:
psjostrom 2020-11-09 11:55:07 +01:00 committed by GitHub
commit 8ea3d6ae74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 397 additions and 80 deletions

52
.vscode/launch.json vendored
View File

@ -47,7 +47,31 @@
]
},
{
"name": "mana",
"name": "build corp",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
},
"preLaunchTask": "build",
"args": [
".",
"--url=https://corporate.symphony.com"
],
"env": {
"ELECTRON_DEBUGGING": "true",
"ELECTRON_DEV": "true"
},
"outputCapture": "std",
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/lib/**/*.js"
]
},
{
"name": "mana local",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
@ -68,7 +92,31 @@
"outFiles": [
"${workspaceFolder}/lib/**/*.js"
]
}
},
{
"name": "build mana local",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.exe"
},
"preLaunchTask": "build",
"args": [
".",
"--url=https://local-dev.symphony.com:9090"
],
"env": {
"ELECTRON_DEBUGGING": "true",
"ELECTRON_DEV": "true"
},
"outputCapture": "std",
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/lib/**/*.js"
]
},
{
"name": "demo",
"type": "node",

10
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "npx run-s compile browserify"
}
]
}

View File

@ -150,6 +150,7 @@
"typescript": "3.9.7"
},
"dependencies": {
"@types/lazy-brush": "^1.0.0",
"archiver": "3.1.1",
"async.map": "0.5.2",
"classnames": "2.2.6",
@ -160,6 +161,7 @@
"ffi-napi": "3.0.0",
"filesize": "6.1.0",
"image-size": "^0.9.3",
"lazy-brush": "^1.0.1",
"react": "16.13.0",
"react-dom": "16.13.0",
"ref-napi": "1.4.3",

View File

@ -12,6 +12,11 @@ exports[`Snipping Tool should render correctly 1`] = `
<button
className="ActionButton"
onClick={[Function]}
style={
Object {
"border": "2px solid rgba(0, 142, 255, 1)",
}
}
>
<img
src="../renderer/assets/snip-draw.svg"
@ -45,7 +50,9 @@ exports[`Snipping Tool should render correctly 1`] = `
</button>
</div>
</header>
;
<main>
<div>
<img
alt="Screen snippet"
className="SnippetImage"
@ -53,6 +60,7 @@ exports[`Snipping Tool should render correctly 1`] = `
src="Screen-Snippet"
width={800}
/>
</div>
</main>
<footer>
<button

View File

@ -0,0 +1,66 @@
import * as React from 'react';
export interface IColorPickerPillProps {
availableColors: IColor[];
onChange: (color: string) => void;
}
export interface IColor {
rgbaColor: string; // Should be provided as a rgba string i.e. 'rgba(255, 0, 0, 0.3)'
outline?: string; // Should be provided as a rgba string i.e. 'rgba(255, 0, 0, 0.3)'
chosen?: boolean;
}
const ColorPickerPill = (props: IColorPickerPillProps) => {
const getChosenColor = (colors: IColor[]) => {
return colors.find((color) => color.chosen === true);
};
const chosenColor = getChosenColor(props.availableColors);
const ColorDot = (color: IColor) => {
const isChosenColor = color === chosenColor;
const hasOutline = !!color.outline;
const border = 'solid 1px ' + color.outline;
const getWidthAndHeight = () => {
if (isChosenColor) {
return hasOutline ? '22px' : '24px';
}
return hasOutline ? '6px' : '8px';
};
const widthAndHeight = getWidthAndHeight();
const chooseColor = () => {
props.onChange(color.rgbaColor);
};
return (
<div
key={color.rgbaColor}
className='enclosingCircle'
onClick={chooseColor}
data-testid={'colorDot ' + color.rgbaColor}
>
<div
style={{
background: color.rgbaColor,
width: widthAndHeight,
height: widthAndHeight,
cursor: 'pointer',
border: hasOutline ? border : undefined,
}}
className='colorDot'
/>
</div>
);
};
return (
<div className='colorPicker'>
{props.availableColors.map((color) => ColorDot(color))}
</div>
);
};
export default ColorPickerPill;

View File

@ -1,23 +1,79 @@
import { ipcRenderer } from 'electron';
import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
import ColorPickerPill, { IColor } from './color-picker-pill';
const { useState, useEffect } = React;
const { useState, useCallback, useRef, useEffect } = React;
interface IProps {
drawEnabled: boolean;
highlightEnabled: boolean;
eraseEnabled: boolean;
enum Tool {
pen = 'PEN',
highlight = 'HIGHLIGHT',
eraser = 'ERASER',
}
export interface IPath {
points: IPoint[];
color: string;
strokeWidth: number;
shouldShow: boolean;
key: string;
}
export interface IPoint {
x: number;
y: number;
}
export interface ISvgPath {
svgPath: string;
key: string;
strokeWidth: number;
color: string;
shouldShow: boolean;
}
const availablePenColors: IColor[] = [
{ rgbaColor: 'rgba(0, 0, 40, 1)' },
{ rgbaColor: 'rgba(0, 142, 255, 1)' },
{ rgbaColor: 'rgba(38, 196, 58, 1)' },
{ rgbaColor: 'rgba(246, 178, 2, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
];
const availableHighlightColors: IColor[] = [
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
{ rgbaColor: 'rgba(38, 196, 58, 0.64)' },
{ rgbaColor: 'rgba(246, 178, 2, 0.64)' },
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
];
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
const SnippingTool: React.FunctionComponent<IProps> = ({drawEnabled, highlightEnabled, eraseEnabled}) => {
const SnippingTool = () => {
// State and ref preparation functions
const [screenSnippet, setScreenSnippet] = useState('Screen-Snippet');
const [imageDimensions, setImageDimensions] = useState({height: 600, width: 800});
const [imageDimensions, setImageDimensions] = useState({
height: 600,
width: 800,
});
const [paths, setPaths] = useState<IPath[]>([]);
const [chosenTool, setChosenTool] = useState(Tool.pen);
const [annotateAreaLocation, setAnnotateAreaLocation] = useState({
left: 0,
top: 0,
});
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
const [highlightColor, setHighlightColor] = useState(
'rgba(0, 142, 255, 0.64)',
);
const [
shouldRenderHighlightColorPicker,
setShouldRenderHighlightColorPicker,
] = useState(false);
const [shouldRenderPenColorPicker, setShouldRenderPenColorPicker] = useState(
false,
);
const getSnipImageData = (_event, {snipImage, height, width}) => {
const getSnipImageData = ({ }, { snipImage, height, width }) => {
setScreenSnippet(snipImage);
setImageDimensions({ height, width });
};
@ -30,32 +86,114 @@ const SnippingTool: React.FunctionComponent<IProps> = ({drawEnabled, highlightEn
};
}, []);
const annotateRef = useCallback((domNode) => {
if (domNode) {
setAnnotateAreaLocation(domNode.getBoundingClientRect());
}
}, []);
// Hook that alerts clicks outside of the passed ref
const useClickOutsideExaminer = (
colorPickerRf: React.RefObject<HTMLDivElement>,
penRf: React.RefObject<HTMLButtonElement>,
highlightRf: React.RefObject<HTMLButtonElement>,
) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
!colorPickerRf?.current?.contains(event.target as Node) &&
!penRf?.current?.contains(event.target as Node) &&
!highlightRf?.current?.contains(event.target as Node)
) {
setShouldRenderHighlightColorPicker(false);
setShouldRenderPenColorPicker(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [colorPickerRf, penRf, highlightRf]);
};
const colorPickerRef = useRef<HTMLDivElement>(null);
const penRef = useRef<HTMLButtonElement>(null);
const highlightRef = useRef<HTMLButtonElement>(null);
useClickOutsideExaminer(colorPickerRef, penRef, highlightRef);
// State mutating functions
const penColorChosen = (color: string) => {
setPenColor(color);
setShouldRenderPenColorPicker(false);
};
const highlightColorChosen = (color: string) => {
setHighlightColor(color);
setShouldRenderHighlightColorPicker(false);
};
const usePen = () => {
// setTool("pen");
// setShouldRenderPenColorPicker(shouldRenderPenColorPicker => !shouldRenderPenColorPicker);
// setShouldRenderHighlightColorPicker(false);
setChosenTool(Tool.pen);
setShouldRenderPenColorPicker(!shouldRenderPenColorPicker);
setShouldRenderHighlightColorPicker(false);
};
const useHighlight = () => {
// setTool("highlight");
// setShouldRenderHighlightColorPicker(shouldRenderHighlightColorPicker => !shouldRenderHighlightColorPicker);
// setShouldRenderPenColorPicker(false);
setChosenTool(Tool.highlight);
setShouldRenderHighlightColorPicker(!shouldRenderHighlightColorPicker);
setShouldRenderPenColorPicker(false);
};
const useEraser = () => {
// setTool("eraser");
setChosenTool(Tool.eraser);
};
const clear = () => {
// const updPaths = [...paths];
// updPaths.map((p) => {
// p.shouldShow = false;
// return p;
// });
// setPaths(updPaths);
const updPaths = [...paths];
updPaths.map((p) => {
p.shouldShow = false;
return p;
});
setPaths(updPaths);
};
const done = () => {
// Utility functions
const getMousePosition = (e: React.MouseEvent) => {
return {
x: e.pageX - annotateAreaLocation.left,
y: e.pageY - annotateAreaLocation.top,
};
};
const markChosenColor = (colors: IColor[], chosenColor: string) => {
return colors.map((color) => {
if (color.rgbaColor === chosenColor) {
return { ...color, chosen: true };
} else {
return color;
}
});
};
const getBorderStyle = (tool: Tool) => {
if (chosenTool !== tool) {
return undefined;
}
if (chosenTool === Tool.pen) {
return { border: '2px solid ' + penColor };
} else if (chosenTool === Tool.highlight) {
return { border: '2px solid ' + highlightColor };
} else if (chosenTool === Tool.eraser) {
return { border: '2px solid #008EFF' };
}
return undefined;
};
const done = (e) => {
getMousePosition(e);
ipcRenderer.send('upload-snippet', screenSnippet);
};
@ -64,45 +202,64 @@ const SnippingTool: React.FunctionComponent<IProps> = ({drawEnabled, highlightEn
<header>
<div className='DrawActions'>
<button
className={
drawEnabled ? 'ActionButtonSelected' : 'ActionButton'
}
style={getBorderStyle(Tool.pen)}
className='ActionButton'
onClick={usePen}
>
<img src='../renderer/assets/snip-draw.svg' />
</button>
<button
className={
highlightEnabled
? 'ActionButtonSelected'
: 'ActionButton'
}
style={getBorderStyle(Tool.highlight)}
className='ActionButton'
onClick={useHighlight}
>
<img src='../renderer/assets/snip-highlight.svg' />
</button>
<button
className={
eraseEnabled
? 'ActionButtonSelected'
: 'ActionButton'
}
style={getBorderStyle(Tool.eraser)}
className='ActionButton'
onClick={useEraser}
>
<img src='../renderer/assets/snip-erase.svg' />
</button>
</div>
<div className='ClearActions'>
<button
className='ClearButton'
onClick={clear}
>
<button className='ClearButton' onClick={clear}>
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
</button>
</div>
</header>
</header>;
{
shouldRenderPenColorPicker && (
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
availableColors={markChosenColor(availablePenColors, penColor)}
onChange={penColorChosen}
/>
</div>
</div>
)
}
{
shouldRenderHighlightColorPicker && (
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
availableColors={markChosenColor(
availableHighlightColors,
highlightColor,
)}
onChange={highlightColorChosen}
/>
</div>
</div>
)
}
<main>
<div ref={annotateRef}>
<img
src={screenSnippet}
width={imageDimensions.width}
@ -110,6 +267,7 @@ const SnippingTool: React.FunctionComponent<IProps> = ({drawEnabled, highlightEn
className='SnippetImage'
alt={i18n.t('Screen snippet', SNIPPING_TOOL_NAMESPACE)()}
/>
</div>
</main>
<footer>

View File

@ -47,40 +47,27 @@ body {
max-height: 48px;
.ActionButton {
margin-left: 24px;
width: 24px;
height: 24px;
margin-left: 15px;
background: white;
border-radius: 4px;
cursor: pointer;
padding: 4px 0;
border: none;
&:first-child {
margin-left: 0;
}
img {
width: 24px;
height: 24px;
}
}
.ActionButtonSelected {
margin-left: 24px;
background: white;
border-radius: 4px;
cursor: pointer;
padding: 4px 0;
border: 2px solid #008eff;
box-sizing: border-box;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid white;
&:first-child {
margin-left: 0;
}
img {
width: 24px;
height: 24px;
&:focus {
outline: none;
}
}
@ -180,3 +167,41 @@ body {
}
}
}
.colorPicker {
display: flex;
flex-direction: row;
margin: 4px;
align-items: center;
justify-content: center;
width: fit-content;
height: 40px;
background: #FFFFFF;
box-shadow: 0px 4px 16px rgba(15, 27, 36, 0.14), 0px 4px 8px rgba(15, 27, 36, 0.26);
border-radius: 24px;
}
.enclosingCircle {
width: 24px;
height: 24px;
background: #FFFFFF;
border-radius: 50%;
flex: none;
order: 0;
flex-grow: 0;
margin: 8px;
cursor: pointer;
align-items: center;
justify-content: center;
display: flex;
}
.colorDot {
border-radius: 50%;
flex: none;
order: 0;
flex-grow: 0;
width: 8px;
height: 8px;
background: #000028;
}