import { FloatingLayerContext, useFloatingLayer } from '../../components/ui/floating'; import './monacoSetup'; import Editor from '@monaco-editor/react'; import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent, } from 'react'; import { Check, Code2, Copy, Download, Maximize2, Minimize2, Save, X } from 'lucide-react'; import { downloadBytes } from '../../project/cachedFiles'; import { Button, ConfirmDialog, IconButton } from '../../components/ui'; function basename(path: string): string { return path.split('/').at(-1) ?? path; } function contentSize(content: string): string { const bytes = new Blob([content]).size; return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`; } function xmlProblem(code: string): string | undefined { const document = new DOMParser().parseFromString(code, 'application/xml'), error = document.querySelector('parsererror'); return error?.textContent?.split('\n')[0] || undefined; } export function SourceEditorDialog({ open, code: sourceCode, filePath, theme, onClose, onSave, }: { open: boolean; code: string; filePath: string; theme: 'light' | 'dark'; onClose: () => void; onSave: (path: string, text: string) => void | Promise; }) { const [code, setCode] = useState(sourceCode), [savedCode, setSavedCode] = useState(sourceCode), [saving, setSaving] = useState(false), [saveError, setSaveError] = useState(), [copied, setCopied] = useState(false), [maximized, setMaximized] = useState(false), [discardOpen, setDiscardOpen] = useState(false), [position, setPosition] = useState(() => ({ x: Math.max(24, (window.innerWidth - 900) / 2), y: Math.max(52, (window.innerHeight - 650) / 2), })); const drag = useRef<{ x: number; y: number; left: number; top: number } | null>(null), dialog = useRef(null), previousFocus = useRef(null), dirty = code !== savedCode, problem = xmlProblem(code); const requestClose = useCallback(() => { if (dirty) setDiscardOpen(true); else onClose(); }, [dirty, onClose]); const save = useCallback(async () => { if (!dirty || problem) return; setSaving(true); setSaveError(undefined); try { await onSave(filePath, code); setSavedCode(code); } catch (value) { setSaveError(value instanceof Error ? value.message : String(value)); } finally { setSaving(false); } }, [code, dirty, filePath, onSave, problem]); const layer = useFloatingLayer({ open, roots: () => [dialog.current], dismiss: requestClose }); useEffect(() => { if (!open) return; previousFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; requestAnimationFrame(() => dialog.current?.focus()); return () => { if (previousFocus.current && document.contains(previousFocus.current)) previousFocus.current.focus(); }; }, [open]); useEffect(() => { const key = (event: KeyboardEvent) => { if (!open || discardOpen || !dialog.current?.contains(document.activeElement)) return; if ( (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's' && dirty && !problem ) { event.preventDefault(); void save(); } }; window.addEventListener('keydown', key); return () => window.removeEventListener('keydown', key); }, [open, dirty, discardOpen, problem, requestClose, save]); const copy = async () => { await navigator.clipboard.writeText(code); setCopied(true); window.setTimeout(() => setCopied(false), 1500); }; const download = () => downloadBytes(new TextEncoder().encode(code), basename(filePath), 'application/xml'); const pointerDown = (event: ReactPointerEvent) => { if (maximized || event.button !== 0 || (event.target as HTMLElement).closest('button')) return; drag.current = { x: event.clientX, y: event.clientY, left: position.x, top: position.y }; event.currentTarget.setPointerCapture(event.pointerId); }; const pointerMove = (event: ReactPointerEvent) => { if (!drag.current) return; setPosition({ x: Math.min( window.innerWidth - 120, Math.max(-780, drag.current.left + event.clientX - drag.current.x), ), y: Math.min( window.innerHeight - 48, Math.max(0, drag.current.top + event.clientY - drag.current.y), ), }); }; if (!open) return null; return (
{ drag.current = null; }} onDoubleClick={() => setMaximized((value) => !value)} >
转换后的 MJCF
{filePath}
{contentSize(code)} 缓存文件 · 可编辑 {dirty && ( 已修改 )} setMaximized((value) => !value)} > {maximized ? : }

保存将重新编译并载入模型;未保存的源码关闭后无法恢复。

{saveError && (

{saveError}

)}
setCode(value ?? '')} options={{ automaticLayout: true, minimap: { enabled: false }, fontFamily: "'JetBrains Mono','Fira Code',ui-monospace,monospace", fontSize: 13, fontLigatures: true, scrollBeyondLastLine: false, wordWrap: 'off', stickyScroll: { enabled: false }, tabSize: 2, formatOnPaste: true, formatOnType: true, lineNumbersMinChars: 4, padding: { top: 12, bottom: 14 }, renderLineHighlight: 'all', }} />
{problem ? `XML 错误:${problem}` : '✓ XML 结构正常'}
Ctrl+S 保存并重新载入 MJCF / XML
setDiscardOpen(false)} >

当前 MJCF 源码包含未保存的修改。关闭后,这些修改将无法恢复。

); }