All files / extensions/default/src/ViewerLayout useUndoRedoState.ts

96.96% Statements 32/33
88.88% Branches 8/9
100% Functions 9/9
95.65% Lines 22/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76        220x               220x                       220x           2098x 389x     389x   389x       220x   220x 4133x 4133x 4133x         220x 8892x 3917x       1320x 440x     220x     72x 24x 12x       778x            
import { useEffect, useState } from 'react';
import { eventTarget, utilities as csUtilities } from '@cornerstonejs/core';
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
 
const { DefaultHistoryMemo } = csUtilities.HistoryMemo;
 
// Cornerstone events after which the undo/redo history may have changed.
// `undo`/`redo` emit the HISTORY_* events, but any tool action that records a
// memo (drawing an annotation, editing a labelmap, deleting an annotation)
// pushes onto the history WITHOUT emitting a history event, so we also listen
// for the tool events that produce those memos in order to keep the enabled
// state of the buttons in sync.
const HISTORY_CHANGING_EVENTS = [
  csToolsEnums.Events.HISTORY_UNDO,
  csToolsEnums.Events.HISTORY_REDO,
  csToolsEnums.Events.ANNOTATION_COMPLETED,
  csToolsEnums.Events.ANNOTATION_MODIFIED,
  csToolsEnums.Events.ANNOTATION_REMOVED,
  csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
];
 
// A drag of an existing annotation fires ANNOTATION_MODIFIED only while the
// pointer moves; the tool pushes the memo on mouse up and fires no event for
// it. So also re-read the state after each mouse up / touch end.
const POINTER_END_EVENTS = ['mouseup', 'touchend'];
 
/**
 * Tracks whether an undo/redo is currently available so the header buttons can
 * be disabled (greyed out) when there is nothing to undo/redo.
 */
export function useUndoRedoState(): { canUndo: boolean; canRedo: boolean } {
  const [state, setState] = useState(() => ({
    canUndo: DefaultHistoryMemo.canUndo,
    canRedo: DefaultHistoryMemo.canRedo,
  }));
 
  useEffect(() => {
    // A memo is often pushed synchronously *after* the triggering event is
    // dispatched, so defer the read until the current call stack unwinds to
    // make sure we observe the up-to-date availability.
    let scheduled: ReturnType<typeof setTimeout> | null = null;
 
    const readState = () => {
      scheduled = null;
      setState(prev => {
        const { canUndo, canRedo } = DefaultHistoryMemo;
        return prev.canUndo === canUndo && prev.canRedo === canRedo ? prev : { canUndo, canRedo };
      });
    };
 
    const schedule = () => {
      if (scheduled === null) {
        scheduled = setTimeout(readState, 0);
      }
    };
 
    HISTORY_CHANGING_EVENTS.forEach(evt => eventTarget.addEventListener(evt, schedule));
    POINTER_END_EVENTS.forEach(evt => window.addEventListener(evt, schedule, true));
 
    // Sync once on mount in case the history already has content.
    readState();
 
    return () => {
      HISTORY_CHANGING_EVENTS.forEach(evt => eventTarget.removeEventListener(evt, schedule));
      POINTER_END_EVENTS.forEach(evt => window.removeEventListener(evt, schedule, true));
      Iif (scheduled !== null) {
        clearTimeout(scheduled);
      }
    };
  }, []);
 
  return state;
}
 
export default useUndoRedoState;