From 87de146c07fb6ea654e3c0ac396a2184149ba42a Mon Sep 17 00:00:00 2001 From: psjostrom Date: Fri, 6 Nov 2020 17:17:48 +0100 Subject: [PATCH 1/2] SDA-2402 Work on state for annotate and adding color picker --- .vscode/launch.json | 52 +++- .vscode/tasks.json | 10 + package.json | 2 + src/renderer/components/color-picker-pill.tsx | 66 +++++ src/renderer/components/snipping-tool.tsx | 256 ++++++++++++++---- src/renderer/styles/snipping-tool.less | 69 +++-- 6 files changed, 382 insertions(+), 73 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 src/renderer/components/color-picker-pill.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index 27de8dd5..736e2733 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..62e9bd7d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,10 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "npx run-s compile browserify" + } + ] + } \ No newline at end of file diff --git a/package.json b/package.json index 31508552..37ff9504 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderer/components/color-picker-pill.tsx b/src/renderer/components/color-picker-pill.tsx new file mode 100644 index 00000000..111a614a --- /dev/null +++ b/src/renderer/components/color-picker-pill.tsx @@ -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 ( +
+
+
+ ); + }; + + return ( +
+ {props.availableColors.map((color) => ColorDot(color))} +
+ ); +}; + +export default ColorPickerPill; diff --git a/src/renderer/components/snipping-tool.tsx b/src/renderer/components/snipping-tool.tsx index 5e677ff2..3fa1bdbd 100644 --- a/src/renderer/components/snipping-tool.tsx +++ b/src/renderer/components/snipping-tool.tsx @@ -1,25 +1,81 @@ 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 = ({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([]); + 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}); + setImageDimensions({ height, width }); }; ipcRenderer.on('snipping-tool-data', getSnipImageData); @@ -30,32 +86,114 @@ const SnippingTool: React.FunctionComponent = ({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, + penRf: React.RefObject, + highlightRf: React.RefObject, + ) => { + 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(null); + const penRef = useRef(null); + const highlightRef = useRef(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,52 +202,72 @@ const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEn
-
-
+ ; + + { + shouldRenderPenColorPicker && ( +
+
+ +
+
+ ) + } + { + shouldRenderHighlightColorPicker && ( +
+
+ +
+
+ ) + }
- {i18n.t('Screen +
+ {i18n.t('Screen +
@@ -117,7 +275,7 @@ const SnippingTool: React.FunctionComponent = ({drawEnabled, highlightEn {i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
-
+ ); }; diff --git a/src/renderer/styles/snipping-tool.less b/src/renderer/styles/snipping-tool.less index 66664507..1681c01f 100644 --- a/src/renderer/styles/snipping-tool.less +++ b/src/renderer/styles/snipping-tool.less @@ -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; +} \ No newline at end of file From 153973862a10ddc4e7f252e64b0181c5dcdd6fe7 Mon Sep 17 00:00:00 2001 From: psjostrom Date: Mon, 9 Nov 2020 09:15:07 +0100 Subject: [PATCH 2/2] SDA-2402 Updated snapshot test --- spec/__snapshots__/snippingTool.spec.ts.snap | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/spec/__snapshots__/snippingTool.spec.ts.snap b/spec/__snapshots__/snippingTool.spec.ts.snap index ec9ce78a..d31bedbb 100644 --- a/spec/__snapshots__/snippingTool.spec.ts.snap +++ b/spec/__snapshots__/snippingTool.spec.ts.snap @@ -12,6 +12,11 @@ exports[`Snipping Tool should render correctly 1`] = `