/** * Settings Window - 设置窗口 * 重新设计以匹配编辑器设计稿 */ import { useState, useEffect, useMemo } from 'react'; import { X, Search, Settings as SettingsIcon, ChevronDown, ChevronRight } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; import { SettingsService } from '../services/SettingsService'; import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager, ModuleManifest } from '@esengine/editor-core'; import { PluginListSetting } from './PluginListSetting'; import { ModuleListSetting } from './ModuleListSetting'; import '../styles/SettingsWindow.css'; interface SettingsWindowProps { onClose: () => void; settingsRegistry: SettingsRegistry; initialCategoryId?: string; } // 主分类结构 interface MainCategory { id: string; title: string; subCategories: SettingCategory[]; } export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: SettingsWindowProps) { const [categories, setCategories] = useState([]); const [selectedCategoryId, setSelectedCategoryId] = useState(initialCategoryId || null); const [values, setValues] = useState>(new Map()); const [errors, setErrors] = useState>(new Map()); const [searchTerm, setSearchTerm] = useState(''); const [expandedSections, setExpandedSections] = useState>(new Set()); const [expandedMainCategories, setExpandedMainCategories] = useState>(new Set(['通用'])); // 将分类组织成主分类和子分类 const mainCategories = useMemo((): MainCategory[] => { const categoryMap = new Map(); // 定义主分类映射 const mainCategoryMapping: Record = { 'appearance': '通用', 'general': '通用', 'project': '通用', 'plugins': '通用', 'editor': '通用', 'physics': '全局', 'rendering': '全局', 'audio': '全局', 'world': '世界分区', 'local': '世界分区(本地)', 'performance': '性能' }; categories.forEach((cat) => { const mainCatName = mainCategoryMapping[cat.id] || '其他'; if (!categoryMap.has(mainCatName)) { categoryMap.set(mainCatName, []); } categoryMap.get(mainCatName)!.push(cat); }); // 定义固定的主分类顺序 const orderedMainCategories = [ '通用', '全局', '世界分区', '世界分区(本地)', '性能', '其他' ]; return orderedMainCategories .filter((name) => categoryMap.has(name)) .map((name) => ({ id: name, title: name, subCategories: categoryMap.get(name)! })); }, [categories]); // 获取显示的子分类标题 const subCategoryTitle = useMemo(() => { if (!selectedCategoryId) return ''; const cat = categories.find((c) => c.id === selectedCategoryId); return cat?.title || ''; }, [categories, selectedCategoryId]); // 获取主分类标题 const mainCategoryTitle = useMemo(() => { for (const main of mainCategories) { if (main.subCategories.some((sub) => sub.id === selectedCategoryId)) { return main.title; } } return ''; }, [mainCategories, selectedCategoryId]); useEffect(() => { const allCategories = settingsRegistry.getAllCategories(); setCategories(allCategories); // 默认展开所有section const allSectionIds = new Set(); allCategories.forEach((cat) => { cat.sections.forEach((section) => { allSectionIds.add(`${cat.id}-${section.id}`); }); }); setExpandedSections(allSectionIds); if (allCategories.length > 0 && !selectedCategoryId) { if (initialCategoryId && allCategories.some((c) => c.id === initialCategoryId)) { setSelectedCategoryId(initialCategoryId); } else { const firstCategory = allCategories[0]; if (firstCategory) { setSelectedCategoryId(firstCategory.id); } } } const settings = SettingsService.getInstance(); const projectService = Core.services.tryResolve(ProjectService); const allSettings = settingsRegistry.getAllSettings(); const initialValues = new Map(); for (const [key, descriptor] of allSettings.entries()) { if (key.startsWith('project.') && projectService) { if (key === 'project.uiDesignResolution.width') { const resolution = projectService.getUIDesignResolution(); initialValues.set(key, resolution.width); } else if (key === 'project.uiDesignResolution.height') { const resolution = projectService.getUIDesignResolution(); initialValues.set(key, resolution.height); } else if (key === 'project.uiDesignResolution.preset') { const resolution = projectService.getUIDesignResolution(); initialValues.set(key, `${resolution.width}x${resolution.height}`); } else if (key === 'project.disabledModules') { // Load disabled modules from ProjectService initialValues.set(key, projectService.getDisabledModules()); } else { initialValues.set(key, descriptor.defaultValue); } } else { const value = settings.get(key, descriptor.defaultValue); initialValues.set(key, value); if (key.startsWith('profiler.')) { console.log(`[SettingsWindow] Loading ${key}: stored=${settings.get(key, undefined)}, default=${descriptor.defaultValue}, using=${value}`); } } } console.log('[SettingsWindow] Initial values for profiler:', Array.from(initialValues.entries()).filter(([k]) => k.startsWith('profiler.'))); setValues(initialValues); }, [settingsRegistry, initialCategoryId]); const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => { const newValues = new Map(values); newValues.set(key, value); setValues(newValues); const newErrors = new Map(errors); if (!settingsRegistry.validateSetting(descriptor, value)) { newErrors.set(key, descriptor.validator?.errorMessage || '无效值'); setErrors(newErrors); return; // 验证失败,不保存 } else { newErrors.delete(key); } setErrors(newErrors); // 实时保存设置 const settings = SettingsService.getInstance(); if (!key.startsWith('project.')) { settings.set(key, value); console.log(`[SettingsWindow] Saved ${key}:`, value); // 触发设置变更事件 window.dispatchEvent(new CustomEvent('settings:changed', { detail: { [key]: value } })); } }; const handleSave = async () => { if (errors.size > 0) { return; } const settings = SettingsService.getInstance(); const projectService = Core.services.tryResolve(ProjectService); const changedSettings: Record = {}; let uiResolutionChanged = false; let newWidth = 1920; let newHeight = 1080; let disabledModulesChanged = false; let newDisabledModules: string[] = []; for (const [key, value] of values.entries()) { if (key.startsWith('project.') && projectService) { if (key === 'project.uiDesignResolution.width') { newWidth = value; uiResolutionChanged = true; } else if (key === 'project.uiDesignResolution.height') { newHeight = value; uiResolutionChanged = true; } else if (key === 'project.uiDesignResolution.preset') { const [w, h] = value.split('x').map(Number); if (w && h) { newWidth = w; newHeight = h; uiResolutionChanged = true; } } else if (key === 'project.disabledModules') { newDisabledModules = value as string[]; disabledModulesChanged = true; } changedSettings[key] = value; } else { settings.set(key, value); changedSettings[key] = value; } } if (uiResolutionChanged && projectService) { await projectService.setUIDesignResolution({ width: newWidth, height: newHeight }); } if (disabledModulesChanged && projectService) { await projectService.setDisabledModules(newDisabledModules); } console.log('[SettingsWindow] Saving settings, changedSettings:', changedSettings); window.dispatchEvent(new CustomEvent('settings:changed', { detail: changedSettings })); onClose(); }; const handleCancel = () => { onClose(); }; const handleResetToDefault = () => { const allSettings = settingsRegistry.getAllSettings(); const defaultValues = new Map(); for (const [key, descriptor] of allSettings.entries()) { defaultValues.set(key, descriptor.defaultValue); } setValues(defaultValues); }; const handleExport = () => { const exportData: Record = {}; for (const [key, value] of values.entries()) { exportData[key] = value; } const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'editor-settings.json'; a.click(); URL.revokeObjectURL(url); }; const handleImport = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; try { const text = await file.text(); const importData = JSON.parse(text); const newValues = new Map(values); for (const [key, value] of Object.entries(importData)) { newValues.set(key, value); } setValues(newValues); } catch (err) { console.error('Failed to import settings:', err); } }; input.click(); }; const toggleSection = (sectionId: string) => { setExpandedSections((prev) => { const newSet = new Set(prev); if (newSet.has(sectionId)) { newSet.delete(sectionId); } else { newSet.add(sectionId); } return newSet; }); }; const toggleMainCategory = (categoryId: string) => { setExpandedMainCategories((prev) => { const newSet = new Set(prev); if (newSet.has(categoryId)) { newSet.delete(categoryId); } else { newSet.add(categoryId); } return newSet; }); }; const renderSettingInput = (setting: SettingDescriptor) => { const value = values.get(setting.key) ?? setting.defaultValue; const error = errors.get(setting.key); switch (setting.type) { case 'boolean': return (
{setting.description && ( )} {setting.label}
handleValueChange(setting.key, e.target.checked, setting)} />
); case 'number': return (
{setting.description && ( )} {setting.label}
handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)} placeholder={setting.placeholder} min={setting.min} max={setting.max} step={setting.step} />
); case 'string': return (
{setting.description && ( )} {setting.label}
handleValueChange(setting.key, e.target.value, setting)} placeholder={setting.placeholder} />
); case 'select': return (
{setting.description && ( )} {setting.label}
); case 'range': return (
{setting.description && ( )} {setting.label}
handleValueChange(setting.key, parseFloat(e.target.value), setting)} min={setting.min} max={setting.max} step={setting.step} /> {value}
); case 'color': return (
{setting.description && ( )} {setting.label}
handleValueChange(setting.key, e.target.value, setting)} />
); case 'pluginList': { const pluginManager = Core.services.tryResolve(IPluginManager); if (!pluginManager) { return (

PluginManager 不可用

); } return (
); } case 'collisionMatrix': { const CustomRenderer = setting.customRenderer as React.ComponentType | undefined; if (CustomRenderer) { return (
); } return (

碰撞矩阵编辑器未配置

); } case 'moduleList': { // Get module data from setting's custom props // 从设置的自定义属性获取模块数据 const moduleData = setting as SettingDescriptor & { modules?: ModuleManifest[]; getModules?: () => ModuleManifest[]; useBlacklist?: boolean; validateDisable?: (moduleId: string) => Promise<{ canDisable: boolean; reason?: string }>; }; const moduleValue = value as string[] || []; return (
handleValueChange(setting.key, newValue, setting)} useBlacklist={moduleData.useBlacklist} validateDisable={moduleData.validateDisable} />
); } default: return null; } }; const selectedCategory = categories.find((c) => c.id === selectedCategoryId); return (
e.stopPropagation()}> {/* Left Sidebar */}
编辑器偏好设置
所有设置
{mainCategories.map((mainCat) => (
toggleMainCategory(mainCat.id)} > {expandedMainCategories.has(mainCat.id) ? ( ) : ( )} {mainCat.title}
{expandedMainCategories.has(mainCat.id) && (
{mainCat.subCategories.map((subCat) => ( ))}
)}
))}
{/* Right Content */}
{/* Top Header */}
setSearchTerm(e.target.value)} />
{/* Category Title */}
{mainCategoryTitle} - {subCategoryTitle}
{selectedCategory?.description && (

{selectedCategory.description}

)}
{/* Settings Content */}
{selectedCategory && selectedCategory.sections.map((section) => { const sectionKey = `${selectedCategory.id}-${section.id}`; const isExpanded = expandedSections.has(sectionKey); return (
toggleSection(sectionKey)} > {isExpanded ? ( ) : ( )} {section.title}
{isExpanded && (
{section.settings.map((setting) => (
{renderSettingInput(setting)}
))}
)}
); })} {!selectedCategory && (

请选择一个设置分类

)}
); }