import { Molecule } from 'openchemlib'; import { autoLabel } from 'openchemlib-utils'; import type { ReactNode } from 'react'; import { useCallback } from 'react'; import { FaCopy, FaDatabase, FaDownload, FaFileExport, FaFileImage, FaPaste, FaPlus, FaRegBookmark, FaRegTrashAlt, } from 'react-icons/fa'; import { FaMaximize, FaMinimize } from 'react-icons/fa6'; import { IoOpenOutline } from 'react-icons/io5'; import { MdFlashAuto, MdNumbers, MdOutlineLabelOff } from 'react-icons/md'; import { PanelHeader, Toolbar, TooltipHelpContent } from 'react-science/ui'; import type { MoleculesView, StateMoleculeExtended, } from '../../../data/molecules/Molecule.js'; import { getMolecules, parseErrorMessage, } from '../../../data/molecules/MoleculeManager.js'; import { ClipboardFallbackModal } from '../../../utils/clipboard/clipboardComponents.js'; import { useClipboard } from '../../../utils/clipboard/clipboardHooks.js'; import { useDispatch } from '../../context/DispatchContext.js'; import { useGlobal } from '../../context/GlobalContext.js'; import { usePreferences } from '../../context/PreferencesContext.js'; import { useToaster } from '../../context/ToasterContext.js'; import { useTopicMolecule } from '../../context/TopicMoleculeContext.js'; import type { ToolbarPopoverMenuItem } from '../../elements/ToolbarPopoverItem.js'; import { ToolbarPopoverItem } from '../../elements/ToolbarPopoverItem.js'; import { useDialogToggle } from '../../hooks/useDialogToggle.js'; import AboutPredictionModal from '../../modal/AboutPredictionModal.js'; import { MoleculeAutoLabelsDatabaseModal } from '../../modal/MoleculeAutoLabelsDatabaseModal.js'; import PredictSpectraModal from '../../modal/PredictSpectraModal.js'; import { booleanToString } from '../../utility/booleanToString.js'; import { browserNotSupportedErrorToast, copyPNGToClipboard, exportAsMolfile, exportAsSVG, } from '../../utility/export.js'; import { useMoleculeAnnotationCore } from '../hooks/useMoleculeAnnotationCore.js'; type ExportOperation = | 'CopyAsSmiles' | 'CopyAsMolfileV3' | 'SaveAsMolfileV3' | 'CopyAsMolfileV2' | 'SaveAsMolfileV2' | 'CopyAsPng' | 'SaveAsSvg'; interface ExportDataItem { id: ExportOperation; } const MOL_EXPORT_MENU: Array> = [ { icon: , text: 'Copy as SMILES', data: { id: 'CopyAsSmiles', }, }, { icon: , text: 'Copy as molfile V2', data: { id: 'CopyAsMolfileV2', }, }, { icon: , text: 'Copy as molfile V3', data: { id: 'CopyAsMolfileV3', }, }, { icon: , text: 'Save as molfile V2', data: { id: 'SaveAsMolfileV2', }, }, { icon: , text: 'Save as molfile V3', data: { id: 'SaveAsMolfileV3', }, }, { icon: , text: 'Copy as PNG', data: { id: 'CopyAsPng', }, }, { icon: , text: 'Export as SVG', data: { id: 'SaveAsSvg', }, }, ]; interface MoleculePanelHeaderProps { currentIndex: number; molecules: StateMoleculeExtended[]; moleculesView: MoleculesView; onMoleculeIndexChange?: (index: number) => void; onOpenMoleculeEditor: () => void; renderSource?: 'moleculePanel' | 'predictionPanel'; onClickPreferences?: () => void; onClickPasteMolecule?: () => void; children?: ReactNode; } export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) { const { currentIndex, molecules, moleculesView, onMoleculeIndexChange, onOpenMoleculeEditor, renderSource = 'moleculePanel', onClickPreferences, onClickPasteMolecule, children, } = props; const { rootRef } = useGlobal(); const toaster = useToaster(); const dispatch = useDispatch(); const { current: { defaultMoleculeSettings }, } = usePreferences(); const moleculeKey = molecules?.[currentIndex]?.id; const { dialog, openDialog, closeDialog } = useDialogToggle({ autoLabelDatabaseDialog: false, }); const saveAsSVGHandler = useCallback(() => { if (!rootRef) return; exportAsSVG(`molSVG${currentIndex}`, { rootElement: rootRef, fileName: 'molFile', }); }, [rootRef, currentIndex]); const topicMolecule = useTopicMolecule(); const saveAsPNGHandler = useCallback(async () => { if (!rootRef) return; try { await copyPNGToClipboard(`molSVG${currentIndex}`, { rootElement: rootRef, }); toaster.show({ message: 'MOL copied as PNG to clipboard', intent: 'success', }); } catch (error: unknown) { if (error instanceof Error) { toaster.show({ intent: 'danger', message: error.message }); } else { toaster.show(browserNotSupportedErrorToast); } } }, [rootRef, currentIndex, toaster]); const { rawWriteWithType, readText, shouldFallback, cleanShouldFallback, text, } = useClipboard(); const copyHandler = useCallback( (value: any, title: any) => { void rawWriteWithType(value).then(() => { toaster.show({ message: `${title} copied to clipboard`, intent: 'success', }); }); }, [rawWriteWithType, toaster], ); const exportHandler = useCallback( (selected?: ExportDataItem) => { const m = molecules?.[currentIndex]; const molecule = Molecule.fromMolfile(m.molfile); if (molecule) { switch (selected?.id) { case 'CopyAsSmiles': copyHandler(molecule.toIsomericSmiles(), 'SMILES'); break; case 'CopyAsMolfileV3': copyHandler(m.molfile, 'MOLFile'); break; case 'SaveAsMolfileV3': exportAsMolfile(m.molfile, m.label); break; case 'CopyAsMolfileV2': { copyHandler( molecule.toMolfile({ customLabelPosition: 'normal' }), 'MOLFile', ); break; } case 'SaveAsMolfileV2': { exportAsMolfile( molecule.toMolfile({ customLabelPosition: 'normal' }), m.label, ); break; } case 'CopyAsPng': void saveAsPNGHandler(); break; case 'SaveAsSvg': saveAsSVGHandler(); break; default: break; } } }, [molecules, currentIndex, copyHandler, saveAsPNGHandler, saveAsSVGHandler], ); function handlePasteMoleculeAction() { onClickPasteMolecule?.(); void readText().then(handlePasteMolecule); } /** * The moleecule pasted could be SMILES, molfile or SDF * @param text - text from clipboard * @returns */ async function handlePasteMolecule(text: string | null) { if (!text) return; try { const molecules = getMolecules(text); dispatch({ type: 'ADD_MOLECULES', payload: { molecules, defaultMoleculeSettings }, }); } catch (error: any) { toaster.show({ intent: 'danger', message: error?.message || parseErrorMessage, }); } finally { cleanShouldFallback(); } } const handleDelete = useCallback(() => { const id = molecules[currentIndex]?.id; if (!id) { return; } onMoleculeIndexChange?.(0); dispatch({ type: 'DELETE_MOLECULE', payload: { id, diaIDs: topicMolecule[id].diaIDsAndInfo }, }); }, [molecules, currentIndex, topicMolecule, onMoleculeIndexChange, dispatch]); function floatMoleculeHandler() { dispatch({ type: 'FLOAT_MOLECULE_OVER_SPECTRUM', payload: { id: moleculeKey }, }); } function expandMoleculeHydrogens(expand?: boolean) { const molecule = molecules[currentIndex]; if (!molecule) return; const { id, label } = molecule; const molfile = expand ? topicMolecule[id].toMolfileWithH({ version: 3 }) : topicMolecule[id].toMolfileWithoutH({ version: 3 }); dispatch({ type: 'SET_MOLECULE', payload: { id, label, molfile } }); } function clearCustomAtomLabels() { const currentMolecule = molecules[currentIndex]; if (!currentMolecule) return; const { id, label, molfile } = currentMolecule; const molecule = Molecule.fromMolfile(molfile); for (let atom = 0; atom < molecule.getAllAtoms(); atom++) { molecule.setAtomCustomLabel(atom, null); } dispatch({ type: 'SET_MOLECULE', payload: { id, label, molfile: molecule.toMolfileV3() }, }); } function autoLabels() { const currentMolecule = molecules[currentIndex]; if (!currentMolecule) return; const { id, label, molfile } = currentMolecule; const molecule = Molecule.fromMolfile(molfile); autoLabel(molecule); dispatch({ type: 'SET_MOLECULE', payload: { id, label, molfile: molecule.toMolfileV3() }, }); } const hasMolecules = molecules && molecules.length > 0; const showCounter = hasMolecules && renderSource !== 'predictionPanel'; const moreMenu: ToolbarPopoverMenuItem[] = [ { icon: , text: 'Expand all hydrogens', disabled: !hasMolecules, onClick: () => expandMoleculeHydrogens(true), }, { icon: , text: 'Collapse all hydrogens', disabled: !hasMolecules, onClick: () => expandMoleculeHydrogens(false), }, { icon: , text: 'Clear custom atom labels', disabled: !hasMolecules, onClick: () => clearCustomAtomLabels(), }, { icon: , text: 'Auto label atoms', disabled: !hasMolecules, tooltip: { title: 'Auto label atoms', description: 'Atoms are automatically labeled according to a predefined template database', link: 'https://docs.nmrium.org/help/structure-labelling', }, onClick: () => autoLabels(), }, { icon: , text: 'Template database', onClick: () => openDialog('autoLabelDatabaseDialog'), }, ]; const { handleChangeAtomAnnotation, isAnnotation } = useMoleculeAnnotationCore(moleculeKey, moleculesView[moleculeKey]); return ( {dialog.autoLabelDatabaseDialog && ( )} {renderSource === 'predictionPanel' && } {renderSource === 'moleculePanel' && ( } id="molecule-export-as" /> )} } onClick={handlePasteMoleculeAction} /> {renderSource === 'moleculePanel' && ( } onClick={onOpenMoleculeEditor} /> )} {renderSource === 'moleculePanel' && ( <> } onClick={handleDelete} disabled={!hasMolecules} /> {hasMolecules && ( )} } onClick={() => handleChangeAtomAnnotation('atom-numbers')} active={isAnnotation('atom-numbers')} disabled={!hasMolecules} /> } icon={} onClick={() => handleChangeAtomAnnotation('custom-labels')} active={isAnnotation('custom-labels')} disabled={!hasMolecules} /> )} } onClick={floatMoleculeHandler} active={moleculesView?.[moleculeKey]?.floating.visible || false} disabled={!hasMolecules} />
{children}
); }