default single placement for add image, signature and text tools

This commit is contained in:
Anthony Stirling
2026-04-28 11:03:10 +01:00
committed by Anthony Stirling
parent 8daee7303d
commit b70c7b41e3
4 changed files with 70 additions and 2 deletions
@@ -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]
@@ -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 = ({
<Box style={{ marginLeft: "auto" }}>{placementToggleControl}</Box>
</Group>
<Checkbox
checked={placeMultiple}
onChange={(event) => 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}
/>
<Alert color={placementAlert.color} title={placementAlert.title}>
<Text size="sm">{placementAlert.message}</Text>
</Alert>
@@ -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) {
@@ -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 (