@@ -775,6 +775,10 @@ export default function App() {
775775 // desktop's ``SelectCond.with_roi`` (enabled when *any* selected
776776 // object carries a ROI, not only the displayed one).
777777 const [ selectionHasRoi , setSelectionHasRoi ] = useState ( false ) ;
778+ // Whether the ROI clipboard holds a ROI compatible with the active
779+ // panel (signal vs image), so "Paste ROI" can be enabled. Mirrors
780+ // DataLab desktop's per-panel ROI clipboard.
781+ const [ hasRoiClipboard , setHasRoiClipboard ] = useState ( false ) ;
778782 const [ pendingImageGrid , setPendingImageGrid ] = useState < {
779783 sourceIds : string [ ] ;
780784 schema : SchemaWithValues ;
@@ -1207,6 +1211,28 @@ export default function App() {
12071211 } ;
12081212 } , [ runtime , selectedIds , currentId , plotRefreshNonce ] ) ;
12091213
1214+ // Track whether the ROI clipboard holds a ROI matching the active panel
1215+ // kind (signal vs image), so the "Paste ROI" action can be enabled.
1216+ // Re-evaluated when switching panels or after a copy (``plotRefreshNonce``).
1217+ useEffect ( ( ) => {
1218+ if ( ! runtime ) {
1219+ setHasRoiClipboard ( false ) ;
1220+ return ;
1221+ }
1222+ let cancelled = false ;
1223+ runtime
1224+ . hasRoiInClipboard ( treeKind )
1225+ . then ( ( has ) => {
1226+ if ( ! cancelled ) setHasRoiClipboard ( has ) ;
1227+ } )
1228+ . catch ( ( ) => {
1229+ if ( ! cancelled ) setHasRoiClipboard ( false ) ;
1230+ } ) ;
1231+ return ( ) => {
1232+ cancelled = true ;
1233+ } ;
1234+ } , [ runtime , treeKind , plotRefreshNonce ] ) ;
1235+
12101236 // Publish selection / panel snapshots to non-React consumers (the
12111237 // iframe-embedded remote bridge in particular, so host pages can
12121238 // call ``client.getSelection()`` / target the *current* object).
@@ -2562,6 +2588,87 @@ export default function App() {
25622588 }
25632589 } , [ runtime , currentId , selectedIds , refresh ] ) ;
25642590
2591+ const handleRoiCopy = useCallback ( async ( ) => {
2592+ if ( ! runtime || ! currentId ) return ;
2593+ const ok = await runtime . copyObjectRoi ( currentId ) ;
2594+ if ( ok ) setHasRoiClipboard ( true ) ;
2595+ } , [ runtime , currentId ] ) ;
2596+
2597+ const handleRoiPaste = useCallback ( async ( ) => {
2598+ if ( ! runtime ) return ;
2599+ const ids = selectedIds . length ? selectedIds : currentId ? [ currentId ] : [ ] ;
2600+ if ( ids . length === 0 ) return ;
2601+ setBusy ( true ) ;
2602+ try {
2603+ await runtime . pasteObjectRoi ( ids ) ;
2604+ if ( currentId ) setRoi ( await runtime . getSignalRoi ( currentId ) ) ;
2605+ setPlotRefreshNonce ( ( n ) => n + 1 ) ;
2606+ } catch ( err ) {
2607+ await notify ( {
2608+ kind : "error" ,
2609+ title : t ( "Paste ROI" ) ,
2610+ message : err instanceof Error ? err . message : String ( err ) ,
2611+ } ) ;
2612+ } finally {
2613+ setBusy ( false ) ;
2614+ }
2615+ } , [ runtime , selectedIds , currentId , notify ] ) ;
2616+
2617+ const handleRoiImport = useCallback ( async ( ) => {
2618+ if ( ! runtime || ! currentId ) return ;
2619+ const input = document . createElement ( "input" ) ;
2620+ input . type = "file" ;
2621+ input . accept = ".dlabroi,.json" ;
2622+ input . onchange = async ( ) => {
2623+ const file = input . files ?. [ 0 ] ;
2624+ if ( ! file ) return ;
2625+ setBusy ( true ) ;
2626+ try {
2627+ const bytes = new Uint8Array ( await file . arrayBuffer ( ) ) ;
2628+ await runtime . importObjectRoiBytes ( currentId , bytes ) ;
2629+ setRoi ( await runtime . getSignalRoi ( currentId ) ) ;
2630+ setPlotRefreshNonce ( ( n ) => n + 1 ) ;
2631+ } catch ( err ) {
2632+ await notify ( {
2633+ kind : "error" ,
2634+ title : t ( "Import ROI…" ) ,
2635+ message : err instanceof Error ? err . message : String ( err ) ,
2636+ } ) ;
2637+ } finally {
2638+ setBusy ( false ) ;
2639+ }
2640+ } ;
2641+ input . click ( ) ;
2642+ } , [ runtime , currentId , notify ] ) ;
2643+
2644+ const handleRoiExport = useCallback ( async ( ) => {
2645+ if ( ! runtime || ! currentId ) return ;
2646+ const oid = currentId ;
2647+ setBusy ( true ) ;
2648+ try {
2649+ const bytes = await runtime . exportObjectRoiBytes ( oid ) ;
2650+ const blob = new Blob ( [ new Uint8Array ( bytes ) ] , {
2651+ type : "application/json" ,
2652+ } ) ;
2653+ const url = URL . createObjectURL ( blob ) ;
2654+ const a = document . createElement ( "a" ) ;
2655+ a . href = url ;
2656+ a . download = `${ titleForId ( oid ) } .dlabroi` ;
2657+ document . body . appendChild ( a ) ;
2658+ a . click ( ) ;
2659+ document . body . removeChild ( a ) ;
2660+ URL . revokeObjectURL ( url ) ;
2661+ } catch ( err ) {
2662+ await notify ( {
2663+ kind : "error" ,
2664+ title : t ( "Export ROI…" ) ,
2665+ message : err instanceof Error ? err . message : String ( err ) ,
2666+ } ) ;
2667+ } finally {
2668+ setBusy ( false ) ;
2669+ }
2670+ } , [ runtime , currentId , titleForId , notify ] ) ;
2671+
25652672 // ------------------------------------------------------------------
25662673 // Image ROI handlers (Phase 13)
25672674 // ------------------------------------------------------------------
@@ -2697,6 +2804,87 @@ export default function App() {
26972804 }
26982805 } , [ runtime , currentId , selectedIds , refresh ] ) ;
26992806
2807+ const handleImageRoiCopy = useCallback ( async ( ) => {
2808+ if ( ! runtime || ! currentId ) return ;
2809+ const ok = await runtime . copyObjectRoi ( currentId ) ;
2810+ if ( ok ) setHasRoiClipboard ( true ) ;
2811+ } , [ runtime , currentId ] ) ;
2812+
2813+ const handleImageRoiPaste = useCallback ( async ( ) => {
2814+ if ( ! runtime ) return ;
2815+ const ids = selectedIds . length ? selectedIds : currentId ? [ currentId ] : [ ] ;
2816+ if ( ids . length === 0 ) return ;
2817+ setBusy ( true ) ;
2818+ try {
2819+ await runtime . pasteObjectRoi ( ids ) ;
2820+ if ( currentId ) setImageRoi ( await runtime . getImageRoi ( currentId ) ) ;
2821+ setPlotRefreshNonce ( ( n ) => n + 1 ) ;
2822+ } catch ( err ) {
2823+ await notify ( {
2824+ kind : "error" ,
2825+ title : t ( "Paste ROI" ) ,
2826+ message : err instanceof Error ? err . message : String ( err ) ,
2827+ } ) ;
2828+ } finally {
2829+ setBusy ( false ) ;
2830+ }
2831+ } , [ runtime , selectedIds , currentId , notify ] ) ;
2832+
2833+ const handleImageRoiImport = useCallback ( async ( ) => {
2834+ if ( ! runtime || ! currentId ) return ;
2835+ const input = document . createElement ( "input" ) ;
2836+ input . type = "file" ;
2837+ input . accept = ".dlabroi,.json" ;
2838+ input . onchange = async ( ) => {
2839+ const file = input . files ?. [ 0 ] ;
2840+ if ( ! file ) return ;
2841+ setBusy ( true ) ;
2842+ try {
2843+ const bytes = new Uint8Array ( await file . arrayBuffer ( ) ) ;
2844+ await runtime . importObjectRoiBytes ( currentId , bytes ) ;
2845+ setImageRoi ( await runtime . getImageRoi ( currentId ) ) ;
2846+ setPlotRefreshNonce ( ( n ) => n + 1 ) ;
2847+ } catch ( err ) {
2848+ await notify ( {
2849+ kind : "error" ,
2850+ title : t ( "Import ROI…" ) ,
2851+ message : err instanceof Error ? err . message : String ( err ) ,
2852+ } ) ;
2853+ } finally {
2854+ setBusy ( false ) ;
2855+ }
2856+ } ;
2857+ input . click ( ) ;
2858+ } , [ runtime , currentId , notify ] ) ;
2859+
2860+ const handleImageRoiExport = useCallback ( async ( ) => {
2861+ if ( ! runtime || ! currentId ) return ;
2862+ const oid = currentId ;
2863+ setBusy ( true ) ;
2864+ try {
2865+ const bytes = await runtime . exportObjectRoiBytes ( oid ) ;
2866+ const blob = new Blob ( [ new Uint8Array ( bytes ) ] , {
2867+ type : "application/json" ,
2868+ } ) ;
2869+ const url = URL . createObjectURL ( blob ) ;
2870+ const a = document . createElement ( "a" ) ;
2871+ a . href = url ;
2872+ a . download = `${ titleForId ( oid ) } .dlabroi` ;
2873+ document . body . appendChild ( a ) ;
2874+ a . click ( ) ;
2875+ document . body . removeChild ( a ) ;
2876+ URL . revokeObjectURL ( url ) ;
2877+ } catch ( err ) {
2878+ await notify ( {
2879+ kind : "error" ,
2880+ title : t ( "Export ROI…" ) ,
2881+ message : err instanceof Error ? err . message : String ( err ) ,
2882+ } ) ;
2883+ } finally {
2884+ setBusy ( false ) ;
2885+ }
2886+ } , [ runtime , currentId , titleForId , notify ] ) ;
2887+
27002888 /** Open the ROI dialog pre-filled with a single centered rectangle, to
27012889 * let the user define the area to erase. Mirrors DataLab desktop's
27022890 * ``compute_erase`` which prompts the user for a ROI. */
@@ -3573,22 +3761,30 @@ export default function App() {
35733761 : buildImageAnalysisActions ( imageAnalysisEntries , handleAnalysis ) ) ,
35743762 buildPlotResultsAction ( treeKind , handlePlotResults ) ,
35753763 ...( treeKind === "signal"
3576- ? buildRoiActions ( roi , roiEditMode , {
3764+ ? buildRoiActions ( roi , roiEditMode , hasRoiClipboard , {
35773765 onToggleEditMode : handleToggleRoiEditMode ,
35783766 onEditNumerically : handleEditRoi ,
35793767 onExtractEach : handleRoiExtractEach ,
35803768 onExtractMerged : handleRoiExtractMerged ,
3769+ onCopy : handleRoiCopy ,
3770+ onPaste : handleRoiPaste ,
3771+ onImport : handleRoiImport ,
3772+ onExport : handleRoiExport ,
35813773 onRemoveAt : handleRoiRemoveAt ,
35823774 onRemoveAll : handleRoiRemoveAll ,
35833775 } )
3584- : buildImageRoiActions ( imageRoi , imageRoiEditMode , {
3776+ : buildImageRoiActions ( imageRoi , imageRoiEditMode , hasRoiClipboard , {
35853777 onToggleEditMode : handleToggleImageRoiEditMode ,
35863778 onAddRectangle : handleImageAddRectangle ,
35873779 onAddCircle : handleImageAddCircle ,
35883780 onCreateGrid : handleCreateRoiGrid ,
35893781 onEditNumerically : handleImageEditRoi ,
35903782 onExtractEach : handleImageRoiExtractEach ,
35913783 onExtractMerged : handleImageRoiExtractMerged ,
3784+ onCopy : handleImageRoiCopy ,
3785+ onPaste : handleImageRoiPaste ,
3786+ onImport : handleImageRoiImport ,
3787+ onExport : handleImageRoiExport ,
35923788 onRemoveAt : handleImageRoiRemoveAt ,
35933789 onRemoveAll : handleImageRoiRemoveAll ,
35943790 } ) ) ,
@@ -3609,6 +3805,7 @@ export default function App() {
36093805 roiEditMode ,
36103806 imageRoi ,
36113807 imageRoiEditMode ,
3808+ hasRoiClipboard ,
36123809 handleCreateTyped ,
36133810 handleCreateImageTyped ,
36143811 handleAnalysis ,
@@ -3622,6 +3819,10 @@ export default function App() {
36223819 handleToggleRoiEditMode ,
36233820 handleRoiExtractEach ,
36243821 handleRoiExtractMerged ,
3822+ handleRoiCopy ,
3823+ handleRoiPaste ,
3824+ handleRoiImport ,
3825+ handleRoiExport ,
36253826 handleRoiRemoveAt ,
36263827 handleRoiRemoveAll ,
36273828 handleImageAddRectangle ,
@@ -3631,6 +3832,10 @@ export default function App() {
36313832 handleImageRoiExtractEach ,
36323833 handleToggleImageRoiEditMode ,
36333834 handleImageRoiExtractMerged ,
3835+ handleImageRoiCopy ,
3836+ handleImageRoiPaste ,
3837+ handleImageRoiImport ,
3838+ handleImageRoiExport ,
36343839 handleImageRoiRemoveAt ,
36353840 handleImageRoiRemoveAll ,
36363841 handleDistributeOnGrid ,
0 commit comments