diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index beae38e178..16220a0c01 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -251,6 +251,8 @@ title = "How to add images" move = "Move Image" pause = "Pause placement" place = "Place Image" +placeMultiple = "Stay in placement mode after each placement" +placeMultipleDesc = "Place several images in a row instead of exiting after the first." resume = "Resume placement" [addImage.results] @@ -440,6 +442,8 @@ title = "How to add text" move = "Move Text" pause = "Pause placement" place = "Place Text" +placeMultiple = "Stay in placement mode after each placement" +placeMultipleDesc = "Place several text stamps in a row instead of exiting after the first." resume = "Resume placement" [addText.results] @@ -7127,6 +7131,8 @@ title = "How to add signature" move = "Move Signature" pause = "Pause placement" place = "Place Signature" +placeMultiple = "Stay in placement mode after each placement" +placeMultipleDesc = "Place several signatures in a row instead of exiting after the first." resume = "Resume placement" [sign.results] diff --git a/frontend/src/core/components/tools/sign/SignSettings.tsx b/frontend/src/core/components/tools/sign/SignSettings.tsx index 17a348ed82..8090b743e7 100644 --- a/frontend/src/core/components/tools/sign/SignSettings.tsx +++ b/frontend/src/core/components/tools/sign/SignSettings.tsx @@ -11,6 +11,7 @@ import { Tooltip, Group, Box, + Checkbox, } from "@mantine/core"; import { SignParameters } from "@app/hooks/tools/sign/useSignParameters"; import { SuggestedToolsSection } from "@app/components/tools/shared/SuggestedToolsSection"; @@ -92,7 +93,13 @@ const SignSettings = ({ defaultSignatureSource, }: SignSettingsProps) => { const { t } = useTranslation(); - const { isPlacementMode, signaturesApplied, historyApiRef } = useSignature(); + const { + isPlacementMode, + signaturesApplied, + historyApiRef, + placeMultiple, + setPlaceMultiple, + } = useSignature(); const { activeFileIndex } = useViewer(); const [historyAvailability, setHistoryAvailability] = useState({ canUndo: false, @@ -1215,6 +1222,20 @@ const SignSettings = ({ {placementToggleControl} + setPlaceMultiple(event.currentTarget.checked)} + label={translate( + "mode.placeMultiple", + "Stay in placement mode after each placement", + )} + description={translate( + "mode.placeMultipleDesc", + "Place several stamps in a row instead of exiting after the first.", + )} + disabled={disabled} + /> + {placementAlert.message} diff --git a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx index b585eb8d31..dab1ada887 100644 --- a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx @@ -185,7 +185,16 @@ export const SignatureAPIBridge = forwardRef< isPlacementMode, placementPreviewSize, setSignaturesApplied, + placeMultiple, + setPlacementMode, } = useSignature(); + // Track latest placeMultiple in a ref so the long-lived onAnnotationEvent + // subscription always reads the current value without re-subscribing on every + // toggle change (which would race with mid-flight create events). + const placeMultipleRef = useRef(placeMultiple); + useEffect(() => { + placeMultipleRef.current = placeMultiple; + }, [placeMultiple]); const { getZoomState, registerImmediateZoomUpdate } = useViewer(); const documentReady = useDocumentReady(); const [currentZoom, setCurrentZoom] = useState( @@ -555,6 +564,18 @@ export const SignatureAPIBridge = forwardRef< // Mark signatures as not applied when a new signature is placed if (event.type === "create") { setSignaturesApplied(false); + + // Auto-exit placement mode after a single STAMP placement unless the + // user opted into "place multiple". Only stamps trigger this — ink + // strokes (signatureInk) are also create events but a multi-stroke + // signature would break if we deactivated after the first stroke. + const annotationType = + annotation?.type ?? annotation?.object?.type ?? null; + const isStamp = annotationType === PdfAnnotationSubtype.STAMP; + if (isStamp && !placeMultipleRef.current) { + annotationApi.setActiveTool(null); + setPlacementMode(false); + } } const directData = @@ -576,7 +597,13 @@ export const SignatureAPIBridge = forwardRef< return () => { unsubscribe?.(); }; - }, [annotationApi, storeImageData, setSignaturesApplied, documentReady]); + }, [ + annotationApi, + storeImageData, + setSignaturesApplied, + setPlacementMode, + documentReady, + ]); useEffect(() => { if (!isPlacementMode || !documentReady) { diff --git a/frontend/src/core/contexts/SignatureContext.tsx b/frontend/src/core/contexts/SignatureContext.tsx index 1c1a53e05f..0c7263a513 100644 --- a/frontend/src/core/contexts/SignatureContext.tsx +++ b/frontend/src/core/contexts/SignatureContext.tsx @@ -23,6 +23,10 @@ interface SignatureState { signaturesApplied: boolean; // Size (in screen units) we want newly placed signatures to use placementPreviewSize: { width: number; height: number } | null; + // When false (default), placement mode auto-exits after a single stamp is + // dropped so users don't accidentally place duplicates. When true, the user + // opted into staying in placement mode and can drop multiple stamps in a row. + placeMultiple: boolean; } // Signature actions interface @@ -42,6 +46,7 @@ interface SignatureActions { setPlacementPreviewSize: ( size: { width: number; height: number } | null, ) => void; + setPlaceMultiple: (enabled: boolean) => void; } // Combined context interface @@ -62,6 +67,7 @@ const initialState: SignatureState = { isPlacementMode: false, signaturesApplied: true, // Start as true (no signatures placed yet) placementPreviewSize: null, + placeMultiple: false, }; // Provider component @@ -174,6 +180,13 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ [], ); + const setPlaceMultiple = useCallback((enabled: boolean) => { + setState((prev) => ({ + ...prev, + placeMultiple: enabled, + })); + }, []); + // No auto-activation - all modes use manual buttons const contextValue: SignatureContextValue = { @@ -194,6 +207,7 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ getImageData, setSignaturesApplied, setPlacementPreviewSize, + setPlaceMultiple, }; return (