mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
Annotate WIP
This commit is contained in:
parent
8ea3d6ae74
commit
7becbbed94
@ -441,7 +441,7 @@ export class WindowHandler {
|
||||
} else {
|
||||
this.isMana = false;
|
||||
}
|
||||
logger.info('window-handler: isMana: ' + this.isMana);
|
||||
logger.info('window-handler: isMana: ' + this.isMana);
|
||||
|
||||
// Injects custom title bar and snack bar css into the webContents
|
||||
await injectStyles(this.mainWindow, this.isCustomTitleBar);
|
||||
@ -963,14 +963,14 @@ export class WindowHandler {
|
||||
}
|
||||
|
||||
const parentWindow = BrowserWindow.getFocusedWindow();
|
||||
const MIN_HEIGHT = 312;
|
||||
const MIN_WIDTH = 320;
|
||||
const CONTAINER_HEIGHT = 124;
|
||||
const MIN_HEIGHT = 320;
|
||||
const MIN_WIDTH = 312;
|
||||
const CONTAINER_HEIGHT = 120;
|
||||
|
||||
let windowHeight = dimensions?.height
|
||||
? dimensions.height + CONTAINER_HEIGHT
|
||||
: 600;
|
||||
let windowWidth = dimensions?.width || 800;
|
||||
: 320;
|
||||
let windowWidth = dimensions?.width || 312;
|
||||
|
||||
if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) {
|
||||
windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT;
|
||||
|
@ -38,7 +38,7 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
|
||||
return (
|
||||
<div
|
||||
key={color.rgbaColor}
|
||||
className='enclosingCircle'
|
||||
className='EnclosingCircle'
|
||||
onClick={chooseColor}
|
||||
data-testid={'colorDot ' + color.rgbaColor}
|
||||
>
|
||||
@ -50,14 +50,14 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
|
||||
cursor: 'pointer',
|
||||
border: hasOutline ? border : undefined,
|
||||
}}
|
||||
className='colorDot'
|
||||
className='ColorDot'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='colorPicker'>
|
||||
<div className='ColorPicker'>
|
||||
{props.availableColors.map((color) => ColorDot(color))}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { LazyBrush } from 'lazy-brush';
|
||||
import * as React from 'react';
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
import ColorPickerPill, { IColor } from './color-picker-pill';
|
||||
|
||||
const { useState, useCallback, useRef, useEffect } = React;
|
||||
const { useState, useRef, useEffect } = React;
|
||||
|
||||
enum Tool {
|
||||
pen = 'PEN',
|
||||
@ -32,6 +33,16 @@ export interface ISvgPath {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
const lazy = new LazyBrush({
|
||||
radius: 3,
|
||||
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;
|
||||
const availablePenColors: IColor[] = [
|
||||
{ rgbaColor: 'rgba(0, 0, 40, 1)' },
|
||||
{ rgbaColor: 'rgba(0, 142, 255, 1)' },
|
||||
@ -55,12 +66,9 @@ const SnippingTool = () => {
|
||||
height: 600,
|
||||
width: 800,
|
||||
});
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
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)',
|
||||
@ -86,12 +94,6 @@ const SnippingTool = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const annotateRef = useCallback((domNode) => {
|
||||
if (domNode) {
|
||||
setAnnotateAreaLocation(domNode.getBoundingClientRect());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Hook that alerts clicks outside of the passed ref
|
||||
const useClickOutsideExaminer = (
|
||||
colorPickerRf: React.RefObject<HTMLDivElement>,
|
||||
@ -151,21 +153,27 @@ const SnippingTool = () => {
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
const updPaths = [...paths];
|
||||
updPaths.map((p) => {
|
||||
p.shouldShow = false;
|
||||
return p;
|
||||
});
|
||||
setPaths(updPaths);
|
||||
// Clear logic here
|
||||
};
|
||||
|
||||
const maybeErasePath = (key: string) => {
|
||||
// erase logic here
|
||||
return key;
|
||||
};
|
||||
|
||||
const stopDrawing = () => {
|
||||
if (isDrawing) {
|
||||
setIsDrawing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
|
||||
const getMousePosition = (e: React.MouseEvent) => {
|
||||
return {
|
||||
x: e.pageX - annotateAreaLocation.left,
|
||||
y: e.pageY - annotateAreaLocation.top,
|
||||
};
|
||||
// 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 markChosenColor = (colors: IColor[], chosenColor: string) => {
|
||||
@ -192,11 +200,153 @@ const SnippingTool = () => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const done = (e) => {
|
||||
getMousePosition(e);
|
||||
const done = () => {
|
||||
ipcRenderer.send('upload-snippet', screenSnippet);
|
||||
};
|
||||
|
||||
// Render and preparing render functions
|
||||
|
||||
const addHighlightPoint = (paths: IPath[], point: IPoint) => {
|
||||
const activePath = paths[paths.length - 1];
|
||||
const shouldShow = true;
|
||||
const key = 'path' + paths.length;
|
||||
if (!isDrawing) {
|
||||
paths.push({
|
||||
points: [point],
|
||||
color: highlightColor,
|
||||
strokeWidth: HIGHLIGHT_WIDTH,
|
||||
shouldShow,
|
||||
key,
|
||||
});
|
||||
} else {
|
||||
activePath.points.push(point);
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
const addPenPoint = (paths: IPath[], point: IPoint) => {
|
||||
const activePath = paths[paths.length - 1];
|
||||
const shouldShow = true;
|
||||
const key = 'path' + paths.length;
|
||||
if (!isDrawing) {
|
||||
paths.push({
|
||||
points: [point],
|
||||
color: penColor,
|
||||
strokeWidth: PEN_WIDTH,
|
||||
shouldShow,
|
||||
key,
|
||||
});
|
||||
} else {
|
||||
activePath.points.push(point);
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
const addPathPoint = (e: React.MouseEvent) => {
|
||||
const p = [...paths];
|
||||
const mousePos = getMousePosition(e);
|
||||
lazy.update({ x: mousePos.x, y: mousePos.y });
|
||||
const point: IPoint = lazy.getBrushCoordinates();
|
||||
if (chosenTool === Tool.highlight) {
|
||||
setPaths(addHighlightPoint(p, point));
|
||||
} else {
|
||||
setPaths(addPenPoint(p, point));
|
||||
}
|
||||
if (!isDrawing) {
|
||||
setIsDrawing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPath = (path: ISvgPath) => {
|
||||
return (
|
||||
<path
|
||||
pointerEvents={path.shouldShow ? 'visiblePainted' : 'none'}
|
||||
style={{ display: path.shouldShow ? 'block' : 'none' }}
|
||||
key={path.key}
|
||||
stroke={path.color}
|
||||
strokeLinecap='round'
|
||||
strokeWidth={path.strokeWidth || 5}
|
||||
d={path.svgPath}
|
||||
fill='none'
|
||||
onClick={() => maybeErasePath(path.key)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPaths = (paths: ISvgPath[]) => {
|
||||
return paths.map((path) => renderPath(path));
|
||||
};
|
||||
|
||||
const getSvgDot = (point: IPoint) => {
|
||||
const { x, y } = point;
|
||||
// This is the SVG path data for a dot at the location of x, y
|
||||
return (
|
||||
'M ' +
|
||||
x +
|
||||
' ' +
|
||||
y +
|
||||
' m -0.1, 0 a 0.1,0.1 0 1,0 0.2,0 a 0.1,0.1 0 1,0 -0.2,0'
|
||||
);
|
||||
};
|
||||
|
||||
const getSvgPath = (points: IPoint[]) => {
|
||||
let stroke = '';
|
||||
if (points && points.length > 0) {
|
||||
// Start point of path
|
||||
stroke = `M ${points[0].x} ${points[0].y}`;
|
||||
let p1: IPoint;
|
||||
let p2: IPoint;
|
||||
let end: IPoint;
|
||||
// Adding points from points array to SVG curve path
|
||||
for (let i = 1; i < points.length - 2; i += 2) {
|
||||
p1 = points[i];
|
||||
p2 = points[i + 1];
|
||||
end = points[i + 2];
|
||||
stroke += ` C ${p1.x} ${p1.y}, ${p2.x} ${p2.y}, ${end.x} ${end.y}`;
|
||||
}
|
||||
}
|
||||
return stroke;
|
||||
};
|
||||
|
||||
const getSvgPathData = (path: IPath) => {
|
||||
const points = path.points;
|
||||
const x = points[0].x;
|
||||
const y = points[0].y;
|
||||
let data: string;
|
||||
// Since a path must got from point A to point B, we need at least two X and Y pairs to render something.
|
||||
// Therefore we start with render a dot, so that the user gets visual feedback from only one X and Y pair.
|
||||
data = getSvgDot({ x, y });
|
||||
data += getSvgPath(points);
|
||||
|
||||
return {
|
||||
svgPath: data,
|
||||
key: path && path.key,
|
||||
strokeWidth: path && path.strokeWidth,
|
||||
color: path && path.color,
|
||||
shouldShow: path && path.shouldShow,
|
||||
};
|
||||
};
|
||||
|
||||
const getSvgPathsData = (paths: IPath[]) => {
|
||||
return paths.map((path) => getSvgPathData(path));
|
||||
};
|
||||
|
||||
// Mouse tracking functions
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (chosenTool === Tool.eraser) {
|
||||
return;
|
||||
}
|
||||
addPathPoint(e);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDrawing || chosenTool === Tool.eraser) {
|
||||
return;
|
||||
}
|
||||
addPathPoint(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='SnippingTool' lang={i18n.getLocale()}>
|
||||
<header>
|
||||
@ -205,6 +355,7 @@ const SnippingTool = () => {
|
||||
style={getBorderStyle(Tool.pen)}
|
||||
className='ActionButton'
|
||||
onClick={usePen}
|
||||
title={i18n.t('Pen', SNIPPING_TOOL_NAMESPACE)()}
|
||||
>
|
||||
<img src='../renderer/assets/snip-draw.svg' />
|
||||
</button>
|
||||
@ -212,6 +363,7 @@ const SnippingTool = () => {
|
||||
style={getBorderStyle(Tool.highlight)}
|
||||
className='ActionButton'
|
||||
onClick={useHighlight}
|
||||
title={i18n.t('Highlight', SNIPPING_TOOL_NAMESPACE)()}
|
||||
>
|
||||
<img src='../renderer/assets/snip-highlight.svg' />
|
||||
</button>
|
||||
@ -219,6 +371,7 @@ const SnippingTool = () => {
|
||||
style={getBorderStyle(Tool.eraser)}
|
||||
className='ActionButton'
|
||||
onClick={useEraser}
|
||||
title={i18n.t('Erase', SNIPPING_TOOL_NAMESPACE)()}
|
||||
>
|
||||
<img src='../renderer/assets/snip-erase.svg' />
|
||||
</button>
|
||||
@ -228,7 +381,7 @@ const SnippingTool = () => {
|
||||
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
|
||||
</button>
|
||||
</div>
|
||||
</header>;
|
||||
</header>
|
||||
|
||||
{
|
||||
shouldRenderPenColorPicker && (
|
||||
@ -258,20 +411,34 @@ const SnippingTool = () => {
|
||||
)
|
||||
}
|
||||
|
||||
<main>
|
||||
<div ref={annotateRef}>
|
||||
<img
|
||||
src={screenSnippet}
|
||||
<main style={{ minHeight: MIN_ANNOTATE_AREA_HEIGHT }}>
|
||||
<div>
|
||||
<svg
|
||||
style={{ cursor: 'crosshair' }}
|
||||
id='annotate'
|
||||
width={imageDimensions.width}
|
||||
height={imageDimensions.height}
|
||||
className='SnippetImage'
|
||||
alt={i18n.t('Screen snippet', SNIPPING_TOOL_NAMESPACE)()}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={stopDrawing}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={stopDrawing}
|
||||
>
|
||||
<image
|
||||
x={0}
|
||||
y={0}
|
||||
id='screenSnippet'
|
||||
xlinkHref={screenSnippet}
|
||||
width={imageDimensions.width}
|
||||
height={imageDimensions.height}
|
||||
className='SnippetImage'
|
||||
/>
|
||||
{renderPaths(getSvgPathsData(paths))}
|
||||
</svg>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<button onClick={done}>
|
||||
<button className='DoneButton' onClick={done}>
|
||||
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
|
||||
</button>
|
||||
</footer>
|
||||
|
@ -15,6 +15,13 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
&:focus {
|
||||
user-select: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.SnippingTool:lang(ja-JP) {
|
||||
font-family: @font-family-ja;
|
||||
|
||||
@ -43,8 +50,8 @@ body {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
max-height: 48px;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
|
||||
.ActionButton {
|
||||
width: 24px;
|
||||
@ -65,25 +72,21 @@ body {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ClearButton {
|
||||
display: block;
|
||||
padding: 4px 10px;
|
||||
border: 2px solid #7c7f86;
|
||||
border-radius: 16px;
|
||||
color: #7c7f86;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-left: 24px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: #ffffff;
|
||||
height: 24px;
|
||||
width: 68px;
|
||||
}
|
||||
|
||||
.DrawActions {
|
||||
@ -95,22 +98,20 @@ body {
|
||||
|
||||
.ClearActions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
right: 20px;
|
||||
align-items: center;
|
||||
|
||||
.ActionButton {
|
||||
img {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
background: linear-gradient(
|
||||
@ -119,6 +120,8 @@ body {
|
||||
rgba(255, 255, 255, 0.96)
|
||||
),
|
||||
#525760;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.SnippetImage {
|
||||
width: 100%;
|
||||
@ -129,18 +132,17 @@ body {
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 2px 32px 4px 0;
|
||||
max-height: 72px;
|
||||
height: 72px;
|
||||
|
||||
button {
|
||||
.DoneButton {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 8px 24px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
line-height: 16px;
|
||||
@ -148,27 +150,18 @@ body {
|
||||
color: rgba(255, 255, 255, 0.96);
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
margin: 10px 30px 4px 0;
|
||||
margin-right: 32px;
|
||||
height: 32px;
|
||||
width: 80px;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 10px rgba(61, 162, 253, 1);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 400px) {
|
||||
header {
|
||||
.ClearActions {
|
||||
position: relative;
|
||||
right: auto;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colorPicker {
|
||||
.ColorPicker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 4px;
|
||||
@ -181,7 +174,7 @@ body {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.enclosingCircle {
|
||||
.EnclosingCircle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #FFFFFF;
|
||||
@ -196,7 +189,7 @@ body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.colorDot {
|
||||
.ColorDot {
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
order: 0;
|
||||
|
@ -23,6 +23,7 @@
|
||||
true,
|
||||
650
|
||||
],
|
||||
"no-shadowed-variable": false,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-var-keyword": true,
|
||||
|
Loading…
Reference in New Issue
Block a user