Skip to content

Commit caeb097

Browse files
committed
feat(roi): implement clipboard functionality for ROI copy, paste, import, and export actions
1 parent 99399c2 commit caeb097

6 files changed

Lines changed: 617 additions & 2 deletions

File tree

‎src/App.tsx‎

Lines changed: 207 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)