import { useState, useCallback, useRef, useEffect } from 'react'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useWorkspaceStore } from '../../stores/workspaceStore'; import { useSettingsStore } from '../../stores/settingsStore'; import { useGraphWithHistory } from '../../hooks/useGraphWithHistory'; import { useDocumentHistory } from '../../hooks/useDocumentHistory'; import DocumentManager from '../Workspace/DocumentManager'; import NodeTypeConfigModal from '../Config/NodeTypeConfig'; import EdgeTypeConfigModal from '../Config/EdgeTypeConfig'; import LabelConfigModal from '../Config/LabelConfig'; import TangibleConfigModal from '../Config/TangibleConfig'; import TuioConnectionConfig from '../Config/TuioConnectionConfig'; import BibliographyConfigModal from '../Config/BibliographyConfig'; import InputDialog from '../Common/InputDialog'; import { useConfirm } from '../../hooks/useConfirm'; import { useShortcutLabels } from '../../hooks/useShortcutLabels'; import type { ExportOptions } from '../../utils/graphExport'; /** * MenuBar Component * * Top-level menu bar with dropdown menus for: * - File: Document and workspace operations * - Edit: Configuration and settings * - View: Display and navigation options * - Help: Documentation and keyboard shortcuts */ interface MenuBarProps { onOpenHelp?: () => void; onFitView?: () => void; onExport?: (format: 'png' | 'svg', options?: ExportOptions) => Promise; } const MenuBar: React.FC = ({ onOpenHelp, onFitView, onExport }) => { const [activeMenu, setActiveMenu] = useState(null); const [showDocumentManager, setShowDocumentManager] = useState(false); const [showNodeConfig, setShowNodeConfig] = useState(false); const [showEdgeConfig, setShowEdgeConfig] = useState(false); const [showLabelConfig, setShowLabelConfig] = useState(false); const [showTangibleConfig, setShowTangibleConfig] = useState(false); const [showTuioConfig, setShowTuioConfig] = useState(false); const [showBibliographyConfig, setShowBibliographyConfig] = useState(false); const [showNewDocDialog, setShowNewDocDialog] = useState(false); const [showNewFromTemplateDialog, setShowNewFromTemplateDialog] = useState(false); const menuRef = useRef(null); const { confirm, ConfirmDialogComponent } = useConfirm(); const { getShortcutLabel } = useShortcutLabels(); const { createDocument, createDocumentFromTemplate, activeDocumentId, exportDocument, importDocumentFromFile, switchToDocument, exportAllDocumentsAsZip, exportWorkspace, importWorkspace, } = useWorkspaceStore(); const { clearGraph } = useGraphWithHistory(); const { undo, redo, canUndo, canRedo, undoDescription, redoDescription } = useDocumentHistory(); const { setPresentationMode } = useSettingsStore(); // Listen for custom event to close all menus (e.g., from graph canvas clicks, context menu opens) useEffect(() => { const handleCloseMenuEvent = (event: Event) => { const customEvent = event as CustomEvent; // Don't close if the event came from MenuBar itself (source: 'menubar') if (customEvent.detail?.source !== 'menubar') { setActiveMenu(null); } }; window.addEventListener('closeAllMenus', handleCloseMenuEvent); return () => window.removeEventListener('closeAllMenus', handleCloseMenuEvent); }, []); // Close menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setActiveMenu(null); } }; if (activeMenu) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [activeMenu]); const toggleMenu = useCallback((menuName: string) => { setActiveMenu((current) => { const newMenu = current === menuName ? null : menuName; // When opening a menu (not closing), dispatch event to close context menus after state updates if (newMenu !== null && current !== menuName) { // Use setTimeout to dispatch after the render phase completes setTimeout(() => { window.dispatchEvent(new CustomEvent('closeAllMenus', { detail: { source: 'menubar' } })); }, 0); } return newMenu; }); }, []); const closeMenu = useCallback(() => { setActiveMenu(null); }, []); const handleNewDocument = useCallback(() => { setShowNewDocDialog(true); closeMenu(); }, [closeMenu]); const handleConfirmNewDocument = useCallback((title: string) => { createDocument(title); setShowNewDocDialog(false); }, [createDocument]); const handleNewDocumentFromTemplate = useCallback(() => { if (!activeDocumentId) { alert('Please open a document first to use it as a template'); closeMenu(); return; } setShowNewFromTemplateDialog(true); closeMenu(); }, [activeDocumentId, closeMenu]); const handleConfirmNewFromTemplate = useCallback((title: string) => { if (!activeDocumentId) return; const newDocId = createDocumentFromTemplate(activeDocumentId, title); if (newDocId) { switchToDocument(newDocId); } setShowNewFromTemplateDialog(false); }, [createDocumentFromTemplate, activeDocumentId, switchToDocument]); const handleOpenDocumentManager = useCallback(() => { setShowDocumentManager(true); closeMenu(); }, [closeMenu]); const handleImport = useCallback(async () => { const newDocId = await importDocumentFromFile(); if (newDocId) { switchToDocument(newDocId); } closeMenu(); }, [importDocumentFromFile, switchToDocument, closeMenu]); const handleExport = useCallback(() => { if (activeDocumentId) { exportDocument(activeDocumentId); } closeMenu(); }, [activeDocumentId, exportDocument, closeMenu]); const handleExportAll = useCallback(() => { exportAllDocumentsAsZip(); closeMenu(); }, [exportAllDocumentsAsZip, closeMenu]); const handleExportWorkspace = useCallback(() => { exportWorkspace(); closeMenu(); }, [exportWorkspace, closeMenu]); const handleImportWorkspace = useCallback(() => { importWorkspace(); closeMenu(); }, [importWorkspace, closeMenu]); const handleConfigureActors = useCallback(() => { setShowNodeConfig(true); closeMenu(); }, [closeMenu]); const handleConfigureRelations = useCallback(() => { setShowEdgeConfig(true); closeMenu(); }, [closeMenu]); const handleConfigureLabels = useCallback(() => { setShowLabelConfig(true); closeMenu(); }, [closeMenu]); const handleConfigureTangibles = useCallback(() => { setShowTangibleConfig(true); closeMenu(); }, [closeMenu]); const handleManageBibliography = useCallback(() => { setShowBibliographyConfig(true); closeMenu(); }, [closeMenu]); const handleUndo = useCallback(() => { undo(); closeMenu(); }, [undo, closeMenu]); const handleRedo = useCallback(() => { redo(); closeMenu(); }, [redo, closeMenu]); const handleClearGraph = useCallback(async () => { const confirmed = await confirm({ title: 'Clear Current Graph', message: 'Are you sure you want to clear the current graph? This will remove all actors and relations from this document.', confirmLabel: 'Clear Graph', severity: 'danger', }); if (confirmed) { clearGraph(); } closeMenu(); }, [clearGraph, closeMenu, confirm]); const handleExportPNG = useCallback(async () => { if (!onExport) return; try { await onExport('png'); closeMenu(); } catch (error) { console.error('PNG export failed:', error); alert('Failed to export graph as PNG'); } }, [onExport, closeMenu]); const handleExportSVG = useCallback(async () => { if (!onExport) return; try { await onExport('svg'); closeMenu(); } catch (error) { console.error('SVG export failed:', error); alert('Failed to export graph as SVG'); } }, [onExport, closeMenu]); return ( <>
{/* File Menu */}
{activeMenu === 'file' && (


)}
{/* Edit Menu */}
{activeMenu === 'edit' && (


)}
{/* View Menu */}
{activeMenu === 'view' && (
{/* Presentation Mode */}
{/* TUIO Connection Settings */}
)}
{/* Help Menu */}
{activeMenu === 'help' && (
)}
{/* Modals */} setShowDocumentManager(false)} /> setShowNodeConfig(false)} /> setShowEdgeConfig(false)} /> setShowLabelConfig(false)} /> setShowTangibleConfig(false)} /> setShowTuioConfig(false)} /> setShowBibliographyConfig(false)} /> {/* Input Dialogs */} setShowNewDocDialog(false)} validateInput={(value) => { if (!value.trim()) return 'Document name cannot be empty'; return null; }} /> setShowNewFromTemplateDialog(false)} validateInput={(value) => { if (!value.trim()) return 'Document name cannot be empty'; return null; }} /> {/* Confirmation Dialog */} {ConfirmDialogComponent} ); }; export default MenuBar;