/* eslint new-cap: ["error", {"newIsCapExceptionPattern": "^mx"}] */ import { useEffect, useState } from 'react' import { useSelector, useDispatch } from 'react-redux' import { Link as RouterLink } from 'react-router-dom' import { Canvg } from 'canvg' import mxGraphFactory from 'mxgraph' import PropTypes from 'prop-types' import beautify from 'xml-beautifier' import { Divider, Drawer, IconButton, List, ListItem, ListItemIcon, ListItemText, Snackbar, Tooltip, useMediaQuery } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import AddBoxOutlinedIcon from '@material-ui/icons/AddBoxOutlined' import ClearAllIcon from '@material-ui/icons/ClearAll' import CloseIcon from '@material-ui/icons/Close' import CreateNewFolderOutlinedIcon from '@material-ui/icons/CreateNewFolderOutlined' import DeleteIcon from '@material-ui/icons/Delete' import DescriptionIcon from '@material-ui/icons/Description' import HelpOutlineIcon from '@material-ui/icons/HelpOutline' import ImageOutlinedIcon from '@material-ui/icons/ImageOutlined' import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser' import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline' import PrintOutlinedIcon from '@material-ui/icons/PrintOutlined' import RedoIcon from '@material-ui/icons/Redo' import RotateRightIcon from '@material-ui/icons/RotateRight' import SaveOutlinedIcon from '@material-ui/icons/SaveOutlined' import SettingsOverscanIcon from '@material-ui/icons/SettingsOverscan' import SystemUpdateAltOutlinedIcon from '@material-ui/icons/SystemUpdateAltOutlined' import UndoIcon from '@material-ui/icons/Undo' import ZoomInIcon from '@material-ui/icons/ZoomIn' import ZoomOutIcon from '@material-ui/icons/ZoomOut' import { closeCompProperties } from '../../redux/componentPropertiesSlice' import { openLocalSch, saveSchematic, setLoadingDiagram, setSchXmlData } from '../../redux/saveSchematicSlice' import { toggleSimulate } from '../../redux/schematicEditorSlice' import store from '../../redux/store' import api from '../../utils/Api' import { transformXcos, saveToFile } from '../../utils/GalleryUtils' import { ClearGrid, PrintPreview, Rotate, deleteComp, editorRedo, editorUndo, editorZoomAct, editorZoomIn, editorZoomOut, renderGalleryXML, saveXml } from './Helper/ToolbarTools' import { HelpScreen, ImageExportDialog, NetlistModal, OpenSchDialog, ScriptScreen } from './ToolbarExtension' const { mxUtils } = new mxGraphFactory() const useStyles = makeStyles((theme) => ({ menuButton: { marginLeft: 'auto', marginRight: theme.spacing(0), padding: theme.spacing(1), [theme.breakpoints.up('lg')]: { display: 'none' } }, tools: { padding: theme.spacing(1), margin: theme.spacing(0, 0.5), color: '#262626' }, pipe: { fontSize: '1.45rem', color: '#d6c4c2', margin: theme.spacing(0, 1.5) } })) // Notification snackbar to give alert messages function SimpleSnackbar ({ open, close, message }) { return (
} />
) } SimpleSnackbar.propTypes = { open: PropTypes.bool, close: PropTypes.func, message: PropTypes.string } export default function SchematicToolbar ({ _mobileClose, gridRef }) { const classes = useStyles() const isAuthenticated = useSelector(state => state.auth.isAuthenticated) const description = useSelector(state => state.saveSchematic.description) const xmlData = useSelector(state => state.saveSchematic.xmlData) const title2 = useSelector(state => state.saveSchematic.title) const scriptDump = useSelector(state => state.saveSchematic.scriptDump) const showDot = useSelector(state => state.saveSchematic.showDot) const dispatch = useDispatch() const isMobile = useMediaQuery('(max-width:600px)') const [drawerOpen, setDrawerOpen] = useState(false) const toggleDrawer = (open) => () => setDrawerOpen(open) // Netlist Modal Control const [open, setOpen] = useState(false) const [netlist] = useState('') const handleNetlistOpen = () => { dispatch(toggleSimulate()) } const handleClose = () => { setOpen(false) } // Control Help dialog window const [helpOpen, setHelpOpen] = useState(false) const [scriptOpen, setScriptOpen] = useState(false) const handleHelpOpen = () => { setHelpOpen(true) } const handleHelpClose = () => { setHelpOpen(false) } const handleSchWinOpen = () => { setScriptOpen(true) } const handleScriptClose = () => { setScriptOpen(false) } // Handle Delete component const handleDeleteComp = () => { deleteComp() dispatch(closeCompProperties()) } // Handle Notification Snackbar const [snacOpen, setSnacOpen] = useState(false) const [message, setMessage] = useState('') const handleSnacClick = () => { setSnacOpen(true) } const handleSnacClose = (event, reason) => { if (reason === 'clickaway') { return } setSnacOpen(false) } useEffect(() => { if (xmlData) { renderGalleryXML(xmlData) } }, [xmlData]) // Image Export of Schematic Diagram async function exportImage (type) { try { const svg = document.querySelector('#divGrid > svg').cloneNode(true) // Ensure xlink namespace is declared in the root SVG element svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink') svg.removeAttribute('style') svg.setAttribute('width', gridRef.current.scrollWidth) svg.setAttribute('height', gridRef.current.scrollHeight) // Create a copy of the SVG for further processing if needed const svgCopyForCanvg = svg.cloneNode(true) const canvas = document.createElement('canvas') canvas.width = gridRef.current.scrollWidth canvas.height = gridRef.current.scrollHeight canvas.style.width = canvas.width + 'px' canvas.style.height = canvas.height + 'px' const images = svgCopyForCanvg.getElementsByTagName('image') for (const image of images) { try { const imageUrl = image.getAttribute('xlink:href') if (imageUrl) { let data = await fetch(imageUrl).then(v => v.text()) data = encodeURIComponent(data) image.removeAttribute('xlink:href') image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'data:image/svg+xml;base64,' + window.btoa(data) ) } } catch (err) { console.error('Error fetching image data:', err) throw err } } const ctx = canvas.getContext('2d') ctx.webkitImageSmoothingEnabled = true ctx.msImageSmoothingEnabled = true ctx.imageSmoothingEnabled = true const pixelRatio = window.devicePixelRatio || 1 ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0) return new Promise(resolve => { if (type === 'SVG') { const svgdata = new XMLSerializer().serializeToString(svg) resolve('' + svgdata) return } let svgString = svg.outerHTML svgString = svgString.replace(/
/g, '
').replace(/
/g, '
').replace(/ /g, ' ') const v = Canvg.fromString(ctx, svgString) v.render().then(() => { let image = '' if (type === 'JPG') { const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height) for (let i = 0; i < imgdata.data.length; i += 4) { if (imgdata.data[i + 3] === 0) { imgdata.data[i] = 255 imgdata.data[i + 1] = 255 imgdata.data[i + 2] = 255 imgdata.data[i + 3] = 255 } } ctx.putImageData(imgdata, 0, 0) image = canvas.toDataURL('image/jpeg', 1.0) } else if (type === 'PNG') { image = canvas.toDataURL('image/png') } resolve(image) }).catch(err => { console.error('Error rendering SVG with Canvg:', err) }) }) } catch (err) { console.error('Error in exportImage function:', err) } } // Download JPEG, PNG exported Image function downloadImage (data, type) { const evt = new MouseEvent('click', { view: window, bubbles: false, cancelable: true }) const a = document.createElement('a') const ext = (type === 'PNG') ? '.png' : '.jpg' a.setAttribute('download', title2 + '_' + process.env.REACT_APP_NAME + '_on_Cloud' + ext) a.setAttribute('href', data) a.setAttribute('target', '_blank') a.dispatchEvent(evt) } // Download SVG image function downloadText (data, options) { saveToFile(title2 + '_' + process.env.REACT_APP_NAME + '_on_Cloud.svg', options, data) } const [imgopen, setImgOpen] = useState(false) const handleImgClickOpen = () => { setImgOpen(true) } const handleImgClose = (value) => { setImgOpen(false) if (value === 'SVG') { exportImage('SVG') .then(v => { downloadText(v, 'data:image/svg+xml') }) } else if (value === 'PNG') { exportImage('PNG') .then(v => { downloadImage(v, 'PNG') }) } else if (value === 'JPG') { exportImage('JPG') .then(v => { downloadImage(v, 'JPG') }) } } // Handle Save Schematic onCloud const handleSchSave = () => { if (isAuthenticated !== true) { setMessage('You are not Logged In') handleSnacClick() } else { const xml = saveXml(description) dispatch(setSchXmlData(xml)) exportImage('PNG') .then(res => { dispatch(saveSchematic({ title: title2, description, xml, base64: res, scriptDump })) setMessage('Saved Successfully') }) .catch(err => { // Debugging: Log if there is an error in exportImage console.error('Error exporting image:', err) setMessage('Error exporting image') }) handleSnacClick() } } // Save Schematics Locally const handleLocalSchSave = () => { saveToFile(title2 + '_' + process.env.REACT_APP_NAME + '_on_Cloud.xml', 'application/xml', beautify(saveXml(description))) } const handleLocalSchSaveXcos = async () => { try { const xmlContent = beautify(saveXml(description)) const xmlBlob = new Blob([xmlContent], { type: 'application/xml' }) const xmlFileName = title2 + '.xml' const scriptTaskId = store.getState().simulation.scriptTaskId const formData = new FormData() formData.append('file', xmlBlob, xmlFileName) formData.append('scriptTaskId', scriptTaskId) const config = { headers: { 'Content-Type': 'multipart/form-data' } } const response = await api.post('/simulation/save', formData, config) if (!response || response.status !== 200) { throw new Error('Network response was not ok') } saveToFile(title2 + '_' + process.env.REACT_APP_NAME + '_on_Cloud.xcos', 'application/x-scilab-xcos', response.data) } catch (error) { console.error('There was an error!', error) } } const handleLocalSchSaveScript = () => { saveToFile(title2 + '_' + process.env.REACT_APP_NAME + '_on_Cloud.sce', 'application/x-scilab', scriptDump) } const readXmlFile = (xmlDoc, dataDump, title) => { const firstCell = xmlDoc.documentElement.children[0].children[0].children[0] const firstCellAttrs = firstCell.attributes const appname = firstCellAttrs.appname.value const description = (firstCellAttrs.description !== undefined) ? firstCellAttrs.description.value : '' if (appname !== process.env.REACT_APP_NAME) { setMessage('Unsupported app name error !') handleSnacClick() } else { const obj = { data_dump: dataDump, title, description } if (obj.data_dump === undefined || obj.title === undefined || obj.description === undefined) { setMessage('Unsupported file error !') handleSnacClick() } else { dispatch(openLocalSch(obj)) } } } // Open Locally Saved Schematic const handleLocalSchOpen = () => { const fileSelector = document.createElement('input') fileSelector.setAttribute('type', 'file') fileSelector.setAttribute('accept', '.xcos, .xml, application/xml') fileSelector.click() fileSelector.addEventListener('change', function (event) { const file = event.target.files[0] const filename = file.name const base = '(_' + process.env.REACT_APP_NAME + '_on_Cloud)?( *\\([0-9]*\\))?\\.(xcos|xml)$' const re = new RegExp(base, 'i') if (re.test(filename)) { const reader = new FileReader() reader.onload = function (event) { dispatch(setLoadingDiagram(true)) const title = filename.replace(re, '') let dataDump = event.target.result const xmlDoc = mxUtils.parseXml(dataDump) const rexcos = /\.xcos$/i if (rexcos.test(filename)) { transformXcos(xmlDoc).then(xmlDoc => { dataDump = new XMLSerializer().serializeToString(xmlDoc) readXmlFile(xmlDoc, dataDump, title) dispatch(setLoadingDiagram(false)) }) } else { readXmlFile(xmlDoc, dataDump, title) dispatch(setLoadingDiagram(false)) } } reader.readAsText(file) } else { setMessage('Unsupported file type error! Select valid file.') handleSnacClick() } }) } // Control Help dialog window open and close const [schOpen, setSchOpen] = useState(false) const handleSchDialOpen = () => { setSchOpen(true) } const handleSchDialClose = () => { setSchOpen(false) } // All toolbar icons in order const toolbarItems = [ { icon: , label: 'New', link: '/editor' }, { icon: , label: 'Open', action: handleSchDialOpen }, { icon: , label: 'Save', action: handleSchSave }, 'pipe', { icon: , label: 'Export', action: handleLocalSchSave }, { icon: , label: 'Export in Xcos', action: handleLocalSchSaveXcos }, { icon: , label: 'Export Script', action: handleLocalSchSaveScript }, { icon: , label: 'Image Export', action: handleImgClickOpen }, { icon: , label: 'Print Preview', action: PrintPreview }, 'pipe', { icon: (
{/* Blinking Dot */} {showDot && (
)} {/* CSS for blinking effect */}
), label: 'Show Script', action: handleSchWinOpen }, { icon: , label: 'Simulate', action: handleNetlistOpen }, 'pipe', { icon: , label: 'Undo', action: editorUndo }, { icon: , label: 'Redo', action: editorRedo }, { icon: , label: 'Rotate', action: Rotate }, 'pipe', { icon: , label: 'Zoom In', action: editorZoomIn }, { icon: , label: 'Zoom Out', action: editorZoomOut }, { icon: , label: 'Default Size', action: editorZoomAct }, 'pipe', { icon: , label: 'Delete', action: handleDeleteComp }, { icon: , label: 'Clear All', action: ClearGrid }, { icon: , label: 'Help', action: handleHelpOpen } ] // Only first 7 icons will be shown in mobile view, rest will go into the drawer const visibleIcons = isMobile ? toolbarItems.slice(0, 7) : toolbarItems return ( <>
{/* ✅ Desktop: Show all icons */} {!isMobile && visibleIcons.map((item, index) => item === 'pipe' ? ( | ) : ( {item.link ? ( {item.icon} ) : ( {item.icon} )} ) )} {/* ✅ Mobile: Show only hamburger menu */} {isMobile && ( <> )}
{/* ✅ Mobile Hamburger Drawer */} {toolbarItems.map((item, index) => item === 'pipe' ? : ( {item.icon} ) )} {/* ✅ Dialogs & Modals */} {schOpen && } {snacOpen && } {imgopen && } {open && } {helpOpen && } {scriptOpen && } ) } SchematicToolbar.propTypes = { mobileClose: PropTypes.func, gridRef: PropTypes.object.isRequired }