/* 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
}