diff --git a/frontend/src/components/tools/sign/SignSettings.tsx b/frontend/src/components/tools/sign/SignSettings.tsx index c1530ba0c..d7e64fe87 100644 --- a/frontend/src/components/tools/sign/SignSettings.tsx +++ b/frontend/src/components/tools/sign/SignSettings.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from 'react'; import { useTranslation } from "react-i18next"; -import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert } from '@mantine/core'; +import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert, Modal, ColorSwatch, Menu, ActionIcon, Slider } from '@mantine/core'; import ButtonSelector from "../../shared/ButtonSelector"; import { SignParameters } from "../../../hooks/tools/sign/useSignParameters"; @@ -11,13 +11,21 @@ interface SignSettingsProps { onActivateDrawMode?: () => void; onActivateSignaturePlacement?: () => void; onDeactivateSignature?: () => void; + onUpdateDrawSettings?: (color: string, size: number) => void; } -const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature }: SignSettingsProps) => { +const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature, onUpdateDrawSettings }: SignSettingsProps) => { const { t } = useTranslation(); const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [signatureImage, setSignatureImage] = useState(null); + const [canvasSignatureData, setCanvasSignatureData] = useState(null); + const [imageSignatureData, setImageSignatureData] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const modalCanvasRef = useRef(null); + const [isModalDrawing, setIsModalDrawing] = useState(false); + const [selectedColor, setSelectedColor] = useState('#000000'); + const [penSize, setPenSize] = useState(2); // Drawing functions for signature canvas const startDrawing = (e: React.MouseEvent) => { @@ -25,11 +33,14 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv setIsDrawing(true); const rect = canvasRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const scaleX = canvasRef.current.width / rect.width; + const scaleY = canvasRef.current.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; const ctx = canvasRef.current.getContext('2d'); if (ctx) { + ctx.strokeStyle = selectedColor; ctx.beginPath(); ctx.moveTo(x, y); } @@ -39,8 +50,10 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv if (!isDrawing || !canvasRef.current || disabled) return; const rect = canvasRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const scaleX = canvasRef.current.width / rect.width; + const scaleY = canvasRef.current.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; const ctx = canvasRef.current.getContext('2d'); if (ctx) { @@ -58,7 +71,15 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv if (canvasRef.current) { const dataURL = canvasRef.current.toDataURL('image/png'); console.log('Saving canvas signature data:', dataURL.substring(0, 50) + '...'); + setCanvasSignatureData(dataURL); onParameterChange('signatureData', dataURL); + + // Auto-activate placement mode after drawing + setTimeout(() => { + if (onActivateSignaturePlacement) { + onActivateSignaturePlacement(); + } + }, 100); } }; @@ -68,10 +89,90 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv const ctx = canvasRef.current.getContext('2d'); if (ctx) { ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + setCanvasSignatureData(null); onParameterChange('signatureData', undefined); } }; + // Modal canvas drawing functions + const startModalDrawing = (e: React.MouseEvent) => { + if (!modalCanvasRef.current) return; + + setIsModalDrawing(true); + const rect = modalCanvasRef.current.getBoundingClientRect(); + const scaleX = modalCanvasRef.current.width / rect.width; + const scaleY = modalCanvasRef.current.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; + + const ctx = modalCanvasRef.current.getContext('2d'); + if (ctx) { + ctx.strokeStyle = selectedColor; + ctx.beginPath(); + ctx.moveTo(x, y); + } + }; + + const drawModal = (e: React.MouseEvent) => { + if (!isModalDrawing || !modalCanvasRef.current) return; + + const rect = modalCanvasRef.current.getBoundingClientRect(); + const scaleX = modalCanvasRef.current.width / rect.width; + const scaleY = modalCanvasRef.current.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; + + const ctx = modalCanvasRef.current.getContext('2d'); + if (ctx) { + ctx.lineTo(x, y); + ctx.stroke(); + } + }; + + const stopModalDrawing = () => { + if (!isModalDrawing) return; + setIsModalDrawing(false); + }; + + const clearModalCanvas = () => { + if (!modalCanvasRef.current) return; + + const ctx = modalCanvasRef.current.getContext('2d'); + if (ctx) { + ctx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height); + } + }; + + const saveModalSignature = () => { + if (!modalCanvasRef.current) return; + + const dataURL = modalCanvasRef.current.toDataURL('image/png'); + setCanvasSignatureData(dataURL); + onParameterChange('signatureData', dataURL); + + // Copy to small canvas for display + if (canvasRef.current) { + const ctx = canvasRef.current.getContext('2d'); + if (ctx) { + const img = new Image(); + img.onload = () => { + ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height); + ctx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height); + }; + img.src = dataURL; + } + } + + setIsModalOpen(false); + + // Auto-activate placement mode after saving modal signature + setTimeout(() => { + if (onActivateSignaturePlacement) { + onActivateSignaturePlacement(); + } + }, 100); + }; + // Handle signature image upload const handleSignatureImageChange = (file: File | null) => { console.log('Image file selected:', file); @@ -80,7 +181,15 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv reader.onload = (e) => { if (e.target?.result) { console.log('Image loaded, saving to signatureData, length:', (e.target.result as string).length); + setImageSignatureData(e.target.result as string); onParameterChange('signatureData', e.target.result as string); + + // Auto-activate placement mode after image upload + setTimeout(() => { + if (onActivateSignaturePlacement) { + onActivateSignaturePlacement(); + } + }, 100); } }; reader.readAsDataURL(file); @@ -90,16 +199,77 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv // Initialize canvas React.useEffect(() => { - if (canvasRef.current && parameters.signatureType === 'draw') { + if (canvasRef.current && parameters.signatureType === 'canvas') { const ctx = canvasRef.current.getContext('2d'); if (ctx) { - ctx.strokeStyle = '#000000'; + ctx.strokeStyle = selectedColor; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; } } - }, [parameters.signatureType]); + }, [parameters.signatureType, selectedColor]); + + // Initialize modal canvas when opened + React.useEffect(() => { + if (modalCanvasRef.current && isModalOpen) { + const ctx = modalCanvasRef.current.getContext('2d'); + if (ctx) { + ctx.strokeStyle = selectedColor; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + } + } + }, [isModalOpen, selectedColor]); + + // Switch signature data based on mode + React.useEffect(() => { + if (parameters.signatureType === 'canvas' && canvasSignatureData) { + onParameterChange('signatureData', canvasSignatureData); + } else if (parameters.signatureType === 'image' && imageSignatureData) { + onParameterChange('signatureData', imageSignatureData); + } + }, [parameters.signatureType, canvasSignatureData, imageSignatureData, onParameterChange]); + + // Initialize draw mode on mount if draw type is selected + React.useEffect(() => { + console.log('SignSettings: Component mounted, initial signatureType:', parameters.signatureType); + if (parameters.signatureType === 'draw' && onActivateDrawMode) { + console.log('SignSettings: Initial activation of draw mode with delay'); + // Add a delay to ensure the API bridge is ready + setTimeout(() => { + onActivateDrawMode(); + }, 500); + } + }, [onActivateDrawMode]); // Only run on mount/when callback changes + + // Auto-activate draw mode when draw type is selected + React.useEffect(() => { + console.log('SignSettings: signatureType changed to:', parameters.signatureType); + if (parameters.signatureType === 'draw') { + console.log('SignSettings: Activating draw mode, onActivateDrawMode:', !!onActivateDrawMode); + if (onActivateDrawMode) { + onActivateDrawMode(); + } + } else if (parameters.signatureType !== 'draw') { + console.log('SignSettings: Deactivating draw mode, onDeactivateSignature:', !!onDeactivateSignature); + if (onDeactivateSignature) { + onDeactivateSignature(); + } + } + }, [parameters.signatureType, onActivateDrawMode, onDeactivateSignature]); + + // Update draw settings when color or pen size changes + React.useEffect(() => { + console.log('SignSettings: Draw settings changed - color:', selectedColor, 'penSize:', penSize, 'signatureType:', parameters.signatureType); + if (parameters.signatureType === 'draw' && onUpdateDrawSettings) { + console.log('SignSettings: Calling onUpdateDrawSettings'); + onUpdateDrawSettings(selectedColor, penSize); + } else { + console.log('SignSettings: Not calling onUpdateDrawSettings - signatureType not draw or function not available'); + } + }, [selectedColor, penSize, parameters.signatureType, onUpdateDrawSettings]); return ( @@ -110,12 +280,16 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv onParameterChange('signatureType', value as 'image' | 'text' | 'draw')} + onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')} options={[ { value: 'draw', label: t('sign.type.draw', 'Draw'), }, + { + value: 'canvas', + label: t('sign.type.canvas', 'Canvas'), + }, { value: 'image', label: t('sign.type.image', 'Image'), @@ -130,20 +304,72 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv {/* Signature Creation based on type */} - {parameters.signatureType === 'draw' && ( - + {parameters.signatureType === 'canvas' && ( + + setIsModalOpen(true)} + disabled={disabled} + title="Expand Canvas" + > + + + {t('sign.draw.title', 'Draw your signature')} - + + + + + + + Select Color + + {['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc'].map((color) => ( + setSelectedColor(color)} + /> + ))} + + + + + - - {t('sign.draw.hint', 'Click and drag to draw your signature')} - )} @@ -199,63 +423,172 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv )} + {/* Direct PDF Drawing */} + {parameters.signatureType === 'draw' && ( + + + Direct PDF Drawing + + Draw signatures and annotations directly on the PDF document. Drawing mode will be activated automatically when you go to the PDF viewer. + + + {/* Drawing Controls */} + + {/* Color Picker */} +
+ Color + + {['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc'].map((color) => ( + setSelectedColor(color)} + /> + ))} + +
+ + {/* Pen Size */} +
+ Pen Size: {penSize}px + +
+
+
+
+ )} + {/* Instructions for placing signature */} - - - {parameters.signatureType === 'draw' && t('sign.instructions.draw', 'Draw your signature above, then click "Draw Directly on PDF" to draw live, or "Place Canvas Signature" to place your drawn signature.')} - {parameters.signatureType === 'image' && t('sign.instructions.image', 'Upload your signature image above, then click "Activate Image Placement" to place it on the PDF.')} - {parameters.signatureType === 'text' && t('sign.instructions.text', 'Enter your name above, then click "Activate Text Signature" to place it on the PDF.')} - + {(parameters.signatureType === 'canvas' || parameters.signatureType === 'image' || parameters.signatureType === 'text') && ( + + + {parameters.signatureType === 'canvas' && 'Draw your signature in the canvas above. Placement mode will activate automatically, or click the buttons below to control placement.'} + {parameters.signatureType === 'image' && 'Upload your signature image above. Placement mode will activate automatically, or click the buttons below to control placement.'} + {parameters.signatureType === 'text' && 'Enter your name above. Placement mode will activate automatically, or click the buttons below to control placement.'} + - - {/* Universal activation button */} - {((parameters.signatureType === 'draw' && parameters.signatureData) || - (parameters.signatureType === 'image' && parameters.signatureData) || - (parameters.signatureType === 'text' && parameters.signerName)) && ( + + {/* Universal activation button */} + {((parameters.signatureType === 'canvas' && parameters.signatureData) || + (parameters.signatureType === 'image' && parameters.signatureData) || + (parameters.signatureType === 'text' && parameters.signerName)) && ( + + )} + + {/* Universal deactivate button */} - )} + - {/* Draw directly mode for draw type */} - {parameters.signatureType === 'draw' && ( - - )} - - {/* Universal deactivate button */} - - - - + + + + + + + ); }; diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx index 56fd31f6d..45fc66672 100644 --- a/frontend/src/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx @@ -9,6 +9,7 @@ export interface SignatureAPI { addTextSignature: (text: string, x: number, y: number, pageIndex: number) => void; activateDrawMode: () => void; activateSignaturePlacementMode: () => void; + updateDrawSettings: (color: string, size: number) => void; deactivateTools: () => void; applySignatureFromParameters: (params: SignParameters) => void; } @@ -63,9 +64,26 @@ export const SignatureAPIBridge = forwardRef { - if (!annotationApi) return; + console.log('SignatureAPIBridge.activateDrawMode called, annotationApi:', !!annotationApi); + if (!annotationApi) { + console.log('No annotationApi available'); + return; + } + + console.log('Setting active tool to ink'); // Activate the built-in ink tool for drawing annotationApi.setActiveTool('ink'); + + // Set default ink tool properties (black color, 2px width) + const activeTool = annotationApi.getActiveTool(); + console.log('Active tool after setting ink:', activeTool); + if (activeTool && activeTool.id === 'ink') { + console.log('Setting ink tool defaults'); + annotationApi.setToolDefaults('ink', { + color: '#000000', + thickness: 2 + }); + } }, activateSignaturePlacementMode: () => { @@ -107,6 +125,28 @@ export const SignatureAPIBridge = forwardRef { + console.log('SignatureAPIBridge.updateDrawSettings called with color:', color, 'size:', size); + if (!annotationApi) { + console.log('No annotationApi available for updateDrawSettings'); + return; + } + + // Always update ink tool defaults regardless of current tool + console.log('Setting ink tool defaults'); + annotationApi.setToolDefaults('ink', { + color: color, + thickness: size + }); + + // If ink tool is currently active, reactivate it to apply settings immediately + const activeTool = annotationApi.getActiveTool(); + console.log('Current active tool:', activeTool); + if (activeTool && activeTool.id === 'ink') { + console.log('Reactivating ink tool to apply new settings'); + annotationApi.setActiveTool('ink'); + } + }, deactivateTools: () => { if (!annotationApi) return; diff --git a/frontend/src/contexts/SignatureContext.tsx b/frontend/src/contexts/SignatureContext.tsx index ff8f18ad5..04e5634de 100644 --- a/frontend/src/contexts/SignatureContext.tsx +++ b/frontend/src/contexts/SignatureContext.tsx @@ -17,6 +17,7 @@ interface SignatureActions { activateDrawMode: () => void; deactivateDrawMode: () => void; activateSignaturePlacementMode: () => void; + updateDrawSettings: (color: string, size: number) => void; } // Combined context interface @@ -60,9 +61,14 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }, []); const activateDrawMode = useCallback(() => { + console.log('SignatureContext.activateDrawMode called, apiRef:', !!signatureApiRef.current); if (signatureApiRef.current) { + console.log('Calling signatureApiRef.current.activateDrawMode()'); signatureApiRef.current.activateDrawMode(); setPlacementMode(true); + console.log('Draw mode activated successfully'); + } else { + console.log('signatureApiRef.current is null - cannot activate draw mode'); } }, [setPlacementMode]); @@ -84,6 +90,16 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children } }, [state.signatureConfig, setPlacementMode]); + const updateDrawSettings = useCallback((color: string, size: number) => { + console.log('SignatureContext.updateDrawSettings called with color:', color, 'size:', size); + console.log('signatureApiRef.current available:', !!signatureApiRef.current); + if (signatureApiRef.current) { + signatureApiRef.current.updateDrawSettings(color, size); + } else { + console.log('signatureApiRef.current is null - cannot update draw settings'); + } + }, []); + // No auto-activation - all modes use manual buttons @@ -95,6 +111,7 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children activateDrawMode, deactivateDrawMode, activateSignaturePlacementMode, + updateDrawSettings, }; return ( diff --git a/frontend/src/hooks/tools/sign/useSignParameters.ts b/frontend/src/hooks/tools/sign/useSignParameters.ts index afe305872..0ef775cdc 100644 --- a/frontend/src/hooks/tools/sign/useSignParameters.ts +++ b/frontend/src/hooks/tools/sign/useSignParameters.ts @@ -9,7 +9,7 @@ export interface SignaturePosition { } export interface SignParameters { - signatureType: 'image' | 'text' | 'draw'; + signatureType: 'image' | 'text' | 'draw' | 'canvas'; signatureData?: string; // Base64 encoded image or text content signaturePosition?: SignaturePosition; reason?: string; @@ -41,6 +41,11 @@ const validateSignParameters = (parameters: SignParameters): boolean => { return false; } + // For canvas signatures, require signature data + if (parameters.signatureType === 'canvas' && !parameters.signatureData) { + return false; + } + // For text signatures, require signer name if (parameters.signatureType === 'text' && !parameters.signerName) { return false; diff --git a/frontend/src/tools/Sign.tsx b/frontend/src/tools/Sign.tsx index bfc03f51d..eb6c15e7a 100644 --- a/frontend/src/tools/Sign.tsx +++ b/frontend/src/tools/Sign.tsx @@ -12,7 +12,7 @@ import { useSignature } from "../contexts/SignatureContext"; const Sign = (props: BaseToolProps) => { const { t } = useTranslation(); const { setWorkbench } = useNavigation(); - const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode } = useSignature(); + const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings } = useSignature(); // Manual sync function const syncSignatureConfig = () => { @@ -41,6 +41,16 @@ const Sign = (props: BaseToolProps) => { } }, [base.selectedFiles.length, setWorkbench]); + // Auto-activate draw mode when files are loaded and draw type is selected + useEffect(() => { + if (base.selectedFiles.length > 0 && base.params.parameters.signatureType === 'draw') { + console.log('Sign: Files loaded with draw mode, activating after delay'); + setTimeout(() => { + activateDrawMode(); + }, 1000); // Give viewer time to initialize + } + }, [base.selectedFiles.length, base.params.parameters.signatureType, activateDrawMode]); + // Sync signature configuration with context useEffect(() => { setSignatureConfig(base.params.parameters); @@ -60,9 +70,10 @@ const Sign = (props: BaseToolProps) => { parameters={base.params.parameters} onParameterChange={base.params.updateParameter} disabled={base.endpointLoading} - onActivateDrawMode={activateDrawMode} + onActivateDrawMode={() => activateDrawMode()} onActivateSignaturePlacement={handleSignaturePlacement} onDeactivateSignature={deactivateDrawMode} + onUpdateDrawSettings={updateDrawSettings} /> ), }); @@ -79,7 +90,7 @@ const Sign = (props: BaseToolProps) => { steps: getSteps(), executeButton: { text: t('sign.submit', 'Sign Document'), - isVisible: base.operation.files.length === 0, + isVisible: false, // Hide the execute button - signatures are placed directly loadingText: t('loading'), onClick: base.handleExecute, disabled: !base.params.validateParameters() || base.selectedFiles.length === 0 || !base.endpointEnabled,