diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 64e7bbbd8..ba3f3a706 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -4097,7 +4097,10 @@ stampSettings = "Stamp Settings" savingCopy = "Preparing download..." saveFailed = "Unable to save copy" saveReady = "Download ready" -selectAndMove = "Select and Move" +selectAndMove = "Select and Edit" +editSelectDescription = "Click an existing annotation to edit its colour, opacity, text, or size." +editStampHint = "To change the image, delete this stamp and add a new one." +editSwitchToSelect = "Switch to Select & Edit to edit this annotation." undo = "Undo" redo = "Redo" diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index 0c9cfc3dd..41c1afcc5 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -25,6 +25,12 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const { selectedTool } = useNavigationState(); const isSignMode = selectedTool === 'sign'; + // Check if we're in any annotation tool that should disable the toggle + const isInAnnotationTool = selectedTool === 'annotate' || selectedTool === 'sign' || selectedTool === 'addImage' || selectedTool === 'addText'; + + // Check if we're on annotate tool to highlight the button + const isAnnotateActive = selectedTool === 'annotate'; + // Don't show any annotation controls in sign mode if (isSignMode) { return null; @@ -35,13 +41,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false {/* Annotation Visibility Toggle */} { viewerContext?.toggleAnnotationsVisibility(); }} - disabled={disabled || currentView !== 'viewer'} + disabled={disabled || currentView !== 'viewer' || isInAnnotationTool} > { setTextColor, setTextBackgroundColor, setNoteBackgroundColor, + setInkColor, + setHighlightColor, + setHighlightOpacity, + setFreehandHighlighterWidth, + setUnderlineColor, + setUnderlineOpacity, + setStrikeoutColor, + setStrikeoutOpacity, + setSquigglyColor, + setSquigglyOpacity, + setShapeStrokeColor, + setShapeFillColor, + setShapeOpacity, + setShapeStrokeOpacity, + setShapeFillOpacity, + setTextAlignment, } = styleActions; useEffect(() => { @@ -150,22 +166,32 @@ const Annotate = (_props: BaseToolProps) => { const historyApi = historyApiRef?.current; if (!historyApi) return; - const updateAvailability = () => { + const updateAvailability = () => setHistoryAvailability({ canUndo: historyApi.canUndo?.() ?? false, canRedo: historyApi.canRedo?.() ?? false, }); - }; updateAvailability(); - if (historyApi.subscribe) { + let interval: ReturnType | undefined; + if (!historyApi.subscribe) { + // Fallback polling in case the history API doesn't support subscriptions + interval = setInterval(updateAvailability, 350); + } else { const unsubscribe = historyApi.subscribe(updateAvailability); - if (typeof unsubscribe === 'function') { - return () => unsubscribe(); - } + return () => { + if (typeof unsubscribe === 'function') { + unsubscribe(); + } + if (interval) clearInterval(interval); + }; } - }, [historyApiRef]); + + return () => { + if (interval) clearInterval(interval); + }; + }, [historyApiRef?.current]); useEffect(() => { if (!viewerContext) return; @@ -295,6 +321,22 @@ const Annotate = (_props: BaseToolProps) => { setTextColor, setTextBackgroundColor, setNoteBackgroundColor, + setInkColor, + setHighlightColor, + setHighlightOpacity, + setFreehandHighlighterWidth, + setUnderlineColor, + setUnderlineOpacity, + setStrikeoutColor, + setStrikeoutOpacity, + setSquigglyColor, + setSquigglyOpacity, + setShapeStrokeColor, + setShapeFillColor, + setShapeOpacity, + setShapeStrokeOpacity, + setShapeFillOpacity, + setTextAlignment, }); const steps = diff --git a/frontend/src/core/tools/annotate/AnnotationPanel.tsx b/frontend/src/core/tools/annotate/AnnotationPanel.tsx index ce091bcb7..ff26c99ee 100644 --- a/frontend/src/core/tools/annotate/AnnotationPanel.tsx +++ b/frontend/src/core/tools/annotate/AnnotationPanel.tsx @@ -6,6 +6,7 @@ import LocalIcon from '@app/components/shared/LocalIcon'; import { ColorPicker, ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker'; import { ImageUploader } from '@app/components/annotation/shared/ImageUploader'; import { SuggestedToolsSection } from '@app/components/tools/shared/SuggestedToolsSection'; +import { DrawingControls } from '@app/components/annotation/shared/DrawingControls'; import type { AnnotationToolId, AnnotationAPI } from '@app/components/viewer/viewerTypes'; interface StyleState { @@ -235,7 +236,14 @@ export function AnnotationPanel(props: AnnotationPanelProps) { const defaultStyleControls = ( - {activeTool === 'stamp' ? ( + {activeTool === 'select' ? ( + <> + {t('annotation.selectAndMove', 'Select and Edit')} + + {t('annotation.editSelectDescription', 'Click an existing annotation to edit its color, opacity, text, or size.')} + + + ) : activeTool === 'stamp' ? ( <> {t('annotation.stampSettings', 'Stamp Settings')} )} + {activeTool === 'underline' && ( + + {t('annotation.opacity', 'Opacity')} + + + )} + {activeTool === 'inkHighlighter' && ( {t('annotation.strokeWidth', 'Width')} @@ -530,10 +545,32 @@ export function AnnotationPanel(props: AnnotationPanelProps) { ); - const selectedAnnotationControls = selectedAnn && (() => { - const type = selectedAnn.object?.type; + const selectedDerivedTool = selectedAnn?.object ? deriveToolFromAnnotation(selectedAnn.object) : undefined; + const selectedIsTextMarkup = + (selectedAnn?.object?.type && [9, 10, 11, 12].includes(selectedAnn.object.type)) || + (selectedDerivedTool ? ['highlight', 'underline', 'strikeout', 'squiggly'].includes(selectedDerivedTool) : false); - if ([9, 10, 11, 12].includes(type)) { + const selectedAnnotationControls = selectedAnn && (() => { + const rawType = selectedAnn.object?.type; + const toolId = selectedDerivedTool ?? deriveToolFromAnnotation(selectedAnn.object); + const derivedType = + toolId === 'highlight' ? 9 + : toolId === 'underline' ? 10 + : toolId === 'squiggly' ? 11 + : toolId === 'strikeout' ? 12 + : toolId === 'line' ? 4 + : toolId === 'square' ? 5 + : toolId === 'circle' ? 6 + : toolId === 'polygon' ? 7 + : toolId === 'polyline' ? 8 + : toolId === 'text' ? 3 + : toolId === 'note' ? 3 + : toolId === 'stamp' ? 13 + : toolId === 'ink' ? 15 + : undefined; + const type = typeof rawType === 'number' ? rawType : derivedType; + + if (toolId && ['highlight', 'underline', 'strikeout', 'squiggly'].includes(toolId)) { return ( @@ -569,35 +606,73 @@ export function AnnotationPanel(props: AnnotationPanelProps) { ); } - if (type === 15) { + if (type === 15 || toolId === 'inkHighlighter' || toolId === 'ink') { + const isHighlighter = toolId === 'inkHighlighter'; + const thicknessValue = + selectedAnn.object?.strokeWidth ?? + selectedAnn.object?.borderWidth ?? + selectedAnn.object?.lineWidth ?? + selectedAnn.object?.thickness ?? + (isHighlighter ? freehandHighlighterWidth : inkWidth); + const colorValue = selectedAnn.object?.color ?? (isHighlighter ? highlightColor : inkColor); + const opacityValue = Math.round(((selectedAnn.object?.opacity ?? 1) * 100) || (isHighlighter ? highlightOpacity : 100)); return ( - {t('annotation.editInk', 'Edit Pen')} + + {isHighlighter ? t('annotation.freehandHighlighter', 'Freehand Highlighter') : t('annotation.editInk', 'Edit Pen')} + {t('annotation.color', 'Color')} { - setColorPickerTarget('ink'); + setColorPickerTarget(isHighlighter ? 'highlight' : 'ink'); setIsColorPickerOpen(true); }} /> + {isHighlighter && ( + + {t('annotation.opacity', 'Opacity')} + { + setHighlightOpacity(value); + annotationApiRef?.current?.updateAnnotation?.( + selectedAnn.object?.pageIndex ?? 0, + selectedAnn.object?.id, + { opacity: value / 100 } + ); + }} + /> + + )} {t('annotation.strokeWidth', 'Width')} { annotationApiRef?.current?.updateAnnotation?.( selectedAnn.object?.pageIndex ?? 0, selectedAnn.object?.id, - { strokeWidth: value } + { + strokeWidth: value, + borderWidth: value, + lineWidth: value, + thickness: value, + } ); - setInkWidth(value); + if (isHighlighter) { + setFreehandHighlighterWidth?.(value); + } else { + setInkWidth(value); + } }} /> @@ -606,12 +681,24 @@ export function AnnotationPanel(props: AnnotationPanelProps) { ); } - if (type === 3) { - const derivedTool = deriveToolFromAnnotation(selectedAnn.object); - const isNote = derivedTool === 'note'; + if (type === 3 || toolId === 'text' || toolId === 'note') { + const isNote = toolId === 'note'; const selectedBackground = selectedAnn.object?.backgroundColor ?? (isNote ? noteBackgroundColor || '#ffffff' : textBackgroundColor || '#ffffff'); + const alignValue = selectedAnn.object?.textAlign; + const currentAlign = + typeof alignValue === 'number' + ? alignValue === 1 + ? 'center' + : alignValue === 2 + ? 'right' + : 'left' + : alignValue === 'center' + ? 'center' + : alignValue === 'right' + ? 'right' + : 'left'; return ( @@ -725,12 +812,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) { {t('annotation.textAlignment', 'Text Alignment')} { + setTextAlignment('left'); annotationApiRef?.current?.updateAnnotation?.( selectedAnn.object?.pageIndex ?? 0, selectedAnn.object?.id, - { textAlign: 'left' } + { textAlign: 0 } ); }} size="md" @@ -738,12 +826,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) { { + setTextAlignment('center'); annotationApiRef?.current?.updateAnnotation?.( selectedAnn.object?.pageIndex ?? 0, selectedAnn.object?.id, - { textAlign: 'center' } + { textAlign: 1 } ); }} size="md" @@ -751,12 +840,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) { { + setTextAlignment('right'); annotationApiRef?.current?.updateAnnotation?.( selectedAnn.object?.pageIndex ?? 0, selectedAnn.object?.id, - { textAlign: 'right' } + { textAlign: 2 } ); }} size="md" @@ -770,7 +860,35 @@ export function AnnotationPanel(props: AnnotationPanelProps) { ); } - if (type === 4) { + if (type === 13 || toolId === 'stamp') { + const imageSrc = selectedAnn.object?.imageSrc || selectedAnn.object?.data || selectedAnn.object?.url; + return ( + + + {t('annotation.stamp', 'Add Image')} + {imageSrc ? ( + + {t('annotation.imagePreview', 'Preview')} + {t('annotation.stamp', + + ) : ( + + {t('annotation.unsupportedType', 'This annotation type is not fully supported for editing.')} + + )} + + {t('annotation.editStampHint', 'To change the image, delete this stamp and add a new one.')} + + + + ); + } + + if ([4, 8].includes(type) || toolId === 'line' || toolId === 'polyline') { return ( @@ -826,8 +944,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) { ); } - if ([5, 6, 7].includes(type)) { + if ([5, 6, 7].includes(type) || toolId === 'square' || toolId === 'circle' || toolId === 'polygon') { const shapeName = type === 5 ? 'Square' : type === 6 ? 'Circle' : 'Polygon'; + const strokeColorValue = selectedAnn.object?.strokeColor ?? shapeStrokeColor; + const fillColorValue = selectedAnn.object?.color ?? shapeFillColor; + const opacityValue = Math.round(((selectedAnn.object?.opacity ?? shapeOpacity / 100) * 100) || 100); + const pageIndex = selectedAnn.object?.pageIndex ?? 0; + const annId = selectedAnn.object?.id; return ( @@ -836,7 +959,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) { {t('annotation.strokeColor', 'Stroke Color')} { setColorPickerTarget('shapeStroke'); @@ -847,7 +970,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) { {t('annotation.fillColor', 'Fill Color')} { setColorPickerTarget('shapeFill'); @@ -861,13 +984,18 @@ export function AnnotationPanel(props: AnnotationPanelProps) { { - annotationApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { opacity: value / 100 } - ); + setShapeOpacity(value); + setShapeStrokeOpacity(value); + setShapeFillOpacity(value); + if (annId) { + annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, { + opacity: value / 100, + strokeOpacity: value / 100, + fillOpacity: value / 100, + }); + } }} /> @@ -879,15 +1007,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) { max={12} value={selectedAnn.object?.borderWidth ?? shapeThickness} onChange={(value) => { - annotationApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { + if (annId) { + annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, { borderWidth: value, strokeWidth: value, lineWidth: value, - } - ); + }); + } setShapeThickness(value); }} /> @@ -897,15 +1023,13 @@ export function AnnotationPanel(props: AnnotationPanelProps) { variant={(selectedAnn.object?.borderWidth ?? shapeThickness) === 0 ? 'filled' : 'light'} onClick={() => { const newValue = (selectedAnn.object?.borderWidth ?? shapeThickness) === 0 ? 1 : 0; - annotationApiRef?.current?.updateAnnotation?.( - selectedAnn.object?.pageIndex ?? 0, - selectedAnn.object?.id, - { + if (annId) { + annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, { borderWidth: newValue, strokeWidth: newValue, lineWidth: newValue, - } - ); + }); + } setShapeThickness(newValue); }} > @@ -941,6 +1065,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) { colorPickerTarget !== 'noteBackground' && colorPickerTarget !== 'shapeStroke' && colorPickerTarget !== 'shapeFill' && + colorPickerTarget !== 'underline' && colorPickerTarget !== null } opacity={ @@ -1108,8 +1233,8 @@ export function AnnotationPanel(props: AnnotationPanelProps) { return ( - - + + - {t('annotation.selectAndMove', 'Select and Move')} + {t('annotation.selectAndMove', 'Select and Edit')} - - - - - - - - - - - + @@ -1174,9 +1286,11 @@ export function AnnotationPanel(props: AnnotationPanelProps) { {renderToolButtons(otherTools)} - {!selectedAnn && defaultStyleControls} + {activeTool !== 'select' && defaultStyleControls} - {selectedAnn && selectedAnnotationControls} + {activeTool === 'select' && selectedAnn && selectedAnnotationControls} + + {activeTool === 'select' && !selectedAnn && defaultStyleControls} {colorPickerComponent} diff --git a/frontend/src/core/tools/annotate/useAnnotationSelection.ts b/frontend/src/core/tools/annotate/useAnnotationSelection.ts index ec3891f94..8c84cda99 100644 --- a/frontend/src/core/tools/annotate/useAnnotationSelection.ts +++ b/frontend/src/core/tools/annotate/useAnnotationSelection.ts @@ -10,12 +10,67 @@ interface UseAnnotationSelectionParams { setSelectedTextDraft: (text: string) => void; setSelectedFontSize: (size: number) => void; setInkWidth: (value: number) => void; + setFreehandHighlighterWidth?: (value: number) => void; setShapeThickness: (value: number) => void; setTextColor: (value: string) => void; setTextBackgroundColor: (value: string) => void; setNoteBackgroundColor: (value: string) => void; + setInkColor: (value: string) => void; + setHighlightColor: (value: string) => void; + setHighlightOpacity: (value: number) => void; + setUnderlineColor: (value: string) => void; + setUnderlineOpacity: (value: number) => void; + setStrikeoutColor: (value: string) => void; + setStrikeoutOpacity: (value: number) => void; + setSquigglyColor: (value: string) => void; + setSquigglyOpacity: (value: number) => void; + setShapeStrokeColor: (value: string) => void; + setShapeFillColor: (value: string) => void; + setShapeOpacity: (value: number) => void; + setShapeStrokeOpacity: (value: number) => void; + setShapeFillOpacity: (value: number) => void; + setTextAlignment: (value: 'left' | 'center' | 'right') => void; } +const MARKUP_TOOL_IDS = ['highlight', 'underline', 'strikeout', 'squiggly'] as const; +const DRAWING_TOOL_IDS = ['ink', 'inkHighlighter'] as const; + +const isTextMarkupAnnotation = (annotation: any): boolean => { + const toolId = + annotation?.customData?.annotationToolId || + annotation?.customData?.toolId || + annotation?.object?.customData?.annotationToolId || + annotation?.object?.customData?.toolId; + if (toolId && MARKUP_TOOL_IDS.includes(toolId)) return true; + + const type = annotation?.type ?? annotation?.object?.type; + if (typeof type === 'number' && [9, 10, 11, 12].includes(type)) return true; + + const subtype = annotation?.subtype ?? annotation?.object?.subtype; + if (typeof subtype === 'string') { + const lower = subtype.toLowerCase(); + if (MARKUP_TOOL_IDS.some((t) => lower.includes(t))) return true; + } + return false; +}; + +const shouldStayOnPlacementTool = (annotation: any, derivedTool?: string | null | undefined): boolean => { + const toolId = + derivedTool || + annotation?.customData?.annotationToolId || + annotation?.customData?.toolId || + annotation?.object?.customData?.annotationToolId || + annotation?.object?.customData?.toolId; + + if (toolId && (MARKUP_TOOL_IDS.includes(toolId as any) || DRAWING_TOOL_IDS.includes(toolId as any))) { + return true; + } + const type = annotation?.type ?? annotation?.object?.type; + if (typeof type === 'number' && type === 15) return true; // ink family + if (isTextMarkupAnnotation(annotation)) return true; + return false; +}; + export function useAnnotationSelection({ annotationApiRef, deriveToolFromAnnotation, @@ -29,6 +84,22 @@ export function useAnnotationSelection({ setTextColor, setTextBackgroundColor, setNoteBackgroundColor, + setInkColor, + setHighlightColor, + setHighlightOpacity, + setUnderlineColor, + setUnderlineOpacity, + setStrikeoutColor, + setStrikeoutOpacity, + setSquigglyColor, + setSquigglyOpacity, + setShapeStrokeColor, + setShapeFillColor, + setShapeOpacity, + setShapeStrokeOpacity, + setShapeFillOpacity, + setTextAlignment, + setFreehandHighlighterWidth, }: UseAnnotationSelectionParams) { const [selectedAnn, setSelectedAnn] = useState(null); const [selectedAnnId, setSelectedAnnId] = useState(null); @@ -39,9 +110,12 @@ export function useAnnotationSelection({ const annObject = ann?.object ?? ann ?? null; const annId = annObject?.id ?? null; const type = annObject?.type; + const derivedTool = annObject ? deriveToolFromAnnotation(annObject) : undefined; selectedAnnIdRef.current = annId; setSelectedAnnId(annId); - setSelectedAnn(ann || null); + // Normalize selected annotation to always expose .object for edit panels + const normalizedSelection = ann?.object ? ann : annObject ? { object: annObject } : null; + setSelectedAnn(normalizedSelection); if (annObject?.contents !== undefined) { setSelectedTextDraft(annObject.contents ?? ''); @@ -49,30 +123,105 @@ export function useAnnotationSelection({ if (annObject?.fontSize !== undefined) { setSelectedFontSize(annObject.fontSize ?? 14); } + if (annObject?.textAlign !== undefined) { + const align = annObject.textAlign; + if (typeof align === 'string') { + const normalized = align === 'center' ? 'center' : align === 'right' ? 'right' : 'left'; + setTextAlignment(normalized); + } else if (typeof align === 'number') { + const normalized = align === 1 ? 'center' : align === 2 ? 'right' : 'left'; + setTextAlignment(normalized); + } + } if (type === 3) { - const derivedTool = deriveToolFromAnnotation(annObject); - const background = annObject?.backgroundColor as string | undefined; - if (annObject?.textColor) { - setTextColor(annObject.textColor); + const background = + (annObject?.backgroundColor as string | undefined) || + (annObject?.fillColor as string | undefined) || + undefined; + const textColor = (annObject?.textColor as string | undefined) || (annObject?.color as string | undefined); + if (textColor) { + setTextColor(textColor); } if (derivedTool === 'note') { - if (background) { - setNoteBackgroundColor(background); - } + setNoteBackgroundColor(background || ''); } else { setTextBackgroundColor(background || ''); } } - if (type === 15 && annObject?.strokeWidth !== undefined) { - setInkWidth(annObject.strokeWidth ?? 2); - } else if (type >= 4 && type <= 8 && annObject?.strokeWidth !== undefined) { - setShapeThickness(annObject.strokeWidth ?? 1); + if (type === 15) { + const width = + annObject?.strokeWidth ?? annObject?.borderWidth ?? annObject?.lineWidth ?? annObject?.thickness; + if (derivedTool === 'inkHighlighter') { + if (annObject?.color) setHighlightColor(annObject.color); + if (annObject?.opacity !== undefined) { + setHighlightOpacity(Math.round((annObject.opacity ?? 1) * 100)); + } + if (width !== undefined && setFreehandHighlighterWidth) { + setFreehandHighlighterWidth(width); + } + } else { + if (width !== undefined) setInkWidth(width ?? 2); + if (annObject?.color) { + setInkColor(annObject.color); + } + } + } else if (type >= 4 && type <= 8) { + const width = annObject?.strokeWidth ?? annObject?.borderWidth ?? annObject?.lineWidth; + if (width !== undefined) { + setShapeThickness(width ?? 1); + } } - const matchingTool = deriveToolFromAnnotation(annObject); - if (matchingTool && matchingTool !== activeToolRef.current && !manualToolSwitch.current) { - setActiveTool(matchingTool); + if (type === 9) { + if (annObject?.color) setHighlightColor(annObject.color); + if (annObject?.opacity !== undefined) setHighlightOpacity(Math.round((annObject.opacity ?? 1) * 100)); + } else if (type === 10) { + if (annObject?.color) setUnderlineColor(annObject.color); + if (annObject?.opacity !== undefined) setUnderlineOpacity(Math.round((annObject.opacity ?? 1) * 100)); + } else if (type === 12) { + if (annObject?.color) setStrikeoutColor(annObject.color); + if (annObject?.opacity !== undefined) setStrikeoutOpacity(Math.round((annObject.opacity ?? 1) * 100)); + } else if (type === 11) { + if (annObject?.color) setSquigglyColor(annObject.color); + if (annObject?.opacity !== undefined) setSquigglyOpacity(Math.round((annObject.opacity ?? 1) * 100)); + } + + if ([4, 5, 6, 7, 8].includes(type)) { + const stroke = (annObject?.strokeColor as string | undefined) ?? (annObject?.color as string | undefined); + if (stroke) setShapeStrokeColor(stroke); + if ([5, 6, 7].includes(type)) { + const fill = (annObject?.color as string | undefined) ?? (annObject?.fillColor as string | undefined); + if (fill) setShapeFillColor(fill); + } + const opacity = + annObject?.opacity !== undefined ? Math.round((annObject.opacity ?? 1) * 100) : undefined; + const strokeOpacityValue = + annObject?.strokeOpacity !== undefined + ? Math.round((annObject.strokeOpacity ?? 1) * 100) + : undefined; + const fillOpacityValue = + annObject?.fillOpacity !== undefined ? Math.round((annObject.fillOpacity ?? 1) * 100) : undefined; + if (opacity !== undefined) { + setShapeOpacity(opacity); + setShapeStrokeOpacity(strokeOpacityValue ?? opacity); + setShapeFillOpacity(fillOpacityValue ?? opacity); + } else { + if (strokeOpacityValue !== undefined) setShapeStrokeOpacity(strokeOpacityValue); + if (fillOpacityValue !== undefined) setShapeFillOpacity(fillOpacityValue); + } + } + + const matchingTool = derivedTool; + const stayOnPlacement = shouldStayOnPlacementTool(annObject, matchingTool); + if (matchingTool && activeToolRef.current !== 'select' && !stayOnPlacement) { + activeToolRef.current = 'select'; + setActiveTool('select'); + // Immediately enable select tool to avoid re-entering placement after creation. + annotationApiRef.current?.activateAnnotationTool?.('select'); + } else if (activeToolRef.current === 'select') { + // Keep the viewer in Select mode so clicking existing annotations does not re-enable placement. + annotationApiRef.current?.activateAnnotationTool?.('select'); } }, [ @@ -87,6 +236,23 @@ export function useAnnotationSelection({ setShapeThickness, setTextBackgroundColor, setTextColor, + setInkColor, + setHighlightColor, + setHighlightOpacity, + setUnderlineColor, + setUnderlineOpacity, + setStrikeoutColor, + setStrikeoutOpacity, + setSquigglyColor, + setSquigglyOpacity, + setShapeStrokeColor, + setShapeFillColor, + setShapeOpacity, + setShapeStrokeOpacity, + setShapeFillOpacity, + setTextAlignment, + setFreehandHighlighterWidth, + shouldStayOnPlacementTool, ] ); @@ -94,12 +260,64 @@ export function useAnnotationSelection({ const api = annotationApiRef.current as any; if (!api) return; + const checkSelection = () => { + const ann = api.getSelectedAnnotation?.(); + const currentId = ann?.object?.id ?? ann?.id ?? null; + if (currentId !== selectedAnnIdRef.current) { + applySelectionFromAnnotation(ann ?? null); + } + }; + + let interval: ReturnType | null = null; + if (typeof api.onAnnotationEvent === 'function') { const handler = (event: any) => { const ann = event?.annotation ?? event?.selectedAnnotation ?? null; - switch (event?.type) { + const eventType = event?.type; + switch (eventType) { + case 'create': + case 'add': + case 'added': + case 'created': + case 'annotationCreated': + case 'annotationAdded': + case 'complete': { + const eventAnn = ann ?? api.getSelectedAnnotation?.(); + applySelectionFromAnnotation(eventAnn); + const currentTool = activeToolRef.current; + const tool = + deriveToolFromAnnotation((eventAnn as any)?.object ?? eventAnn ?? api.getSelectedAnnotation?.()) || + currentTool; + const stayOnPlacement = + shouldStayOnPlacementTool(eventAnn, tool) || + (tool ? DRAWING_TOOL_IDS.includes(tool as any) : false); + if (activeToolRef.current !== 'select' && !stayOnPlacement) { + activeToolRef.current = 'select'; + setActiveTool('select'); + annotationApiRef.current?.activateAnnotationTool?.('select'); + } + // Re-read selection after the viewer updates to ensure we have the full annotation object for the edit panel. + setTimeout(() => { + const selected = api.getSelectedAnnotation?.(); + applySelectionFromAnnotation(selected ?? eventAnn ?? null); + const derivedAfter = + deriveToolFromAnnotation((selected as any)?.object ?? selected ?? eventAnn ?? null) || activeToolRef.current; + const stayOnPlacementAfter = + shouldStayOnPlacementTool(selected ?? eventAnn ?? null, derivedAfter) || + (derivedAfter ? DRAWING_TOOL_IDS.includes(derivedAfter as any) : false); + if (activeToolRef.current !== 'select' && !stayOnPlacementAfter) { + activeToolRef.current = 'select'; + setActiveTool('select'); + annotationApiRef.current?.activateAnnotationTool?.('select'); + } + }, 50); + break; + } case 'select': case 'selected': + case 'annotationSelected': + case 'annotationClicked': + case 'annotationTapped': applySelectionFromAnnotation(ann ?? api.getSelectedAnnotation?.()); break; case 'deselect': @@ -127,20 +345,19 @@ export function useAnnotationSelection({ }; const unsubscribe = api.onAnnotationEvent(handler); + interval = setInterval(checkSelection, 450); return () => { if (typeof unsubscribe === 'function') { unsubscribe(); } + if (interval) clearInterval(interval); }; } - const interval = setInterval(() => { - const ann = api.getSelectedAnnotation?.(); - if ((ann?.object?.id ?? null) !== selectedAnnIdRef.current) { - applySelectionFromAnnotation(ann ?? null); - } - }, 350); - return () => clearInterval(interval); + interval = setInterval(checkSelection, 350); + return () => { + if (interval) clearInterval(interval); + }; }, [annotationApiRef, applySelectionFromAnnotation]); return { diff --git a/frontend/src/core/tools/annotate/useAnnotationStyleState.ts b/frontend/src/core/tools/annotate/useAnnotationStyleState.ts index 4dc2302dd..3abd69d74 100644 --- a/frontend/src/core/tools/annotate/useAnnotationStyleState.ts +++ b/frontend/src/core/tools/annotate/useAnnotationStyleState.ts @@ -95,7 +95,7 @@ export const useAnnotationStyleState = ( const [shapeOpacity, setShapeOpacity] = useState(50); const [shapeStrokeOpacity, setShapeStrokeOpacity] = useState(50); const [shapeFillOpacity, setShapeFillOpacity] = useState(50); - const [shapeThickness, setShapeThickness] = useState(1); + const [shapeThickness, setShapeThickness] = useState(2); const buildToolOptions = useCallback( (toolId, extras) => { @@ -135,6 +135,7 @@ export const useAnnotationStyleState = ( const textAlignNumber = textAlignment === 'left' ? 0 : textAlignment === 'center' ? 1 : 2; return { color: textColor, + textColor: textColor, fontSize: textSize, textAlign: textAlignNumber, ...(textBackgroundColor ? { fillColor: textBackgroundColor } : {}),