Files
esengine/packages/editor-app/src/components/MenuBar.tsx

299 lines
9.2 KiB
TypeScript
Raw Normal View History

2025-10-15 18:24:13 +08:00
import { useState, useRef, useEffect } from 'react';
2025-10-15 22:30:49 +08:00
import { UIRegistry, MessageHub, EditorPluginManager } from '@esengine/editor-core';
import type { MenuItem as PluginMenuItem } from '@esengine/editor-core';
2025-10-15 18:24:13 +08:00
import '../styles/MenuBar.css';
interface MenuItem {
label: string;
shortcut?: string;
disabled?: boolean;
separator?: boolean;
submenu?: MenuItem[];
onClick?: () => void;
}
interface MenuBarProps {
locale?: string;
2025-10-15 22:30:49 +08:00
uiRegistry?: UIRegistry;
messageHub?: MessageHub;
pluginManager?: EditorPluginManager;
2025-10-15 18:24:13 +08:00
onNewScene?: () => void;
onOpenScene?: () => void;
onSaveScene?: () => void;
onSaveSceneAs?: () => void;
onOpenProject?: () => void;
onCloseProject?: () => void;
onExit?: () => void;
2025-10-15 20:10:52 +08:00
onOpenPluginManager?: () => void;
2025-10-15 22:30:49 +08:00
onOpenProfiler?: () => void;
onOpenPortManager?: () => void;
2025-10-16 13:07:19 +08:00
onOpenSettings?: () => void;
2025-10-15 20:23:55 +08:00
onToggleDevtools?: () => void;
2025-10-15 18:24:13 +08:00
}
export function MenuBar({
locale = 'en',
2025-10-15 22:30:49 +08:00
uiRegistry,
messageHub,
pluginManager,
2025-10-15 18:24:13 +08:00
onNewScene,
onOpenScene,
onSaveScene,
onSaveSceneAs,
onOpenProject,
onCloseProject,
2025-10-15 20:10:52 +08:00
onExit,
2025-10-15 20:23:55 +08:00
onOpenPluginManager,
2025-10-15 22:30:49 +08:00
onOpenProfiler,
onOpenPortManager,
2025-10-16 13:07:19 +08:00
onOpenSettings,
2025-10-15 20:23:55 +08:00
onToggleDevtools
2025-10-15 18:24:13 +08:00
}: MenuBarProps) {
const [openMenu, setOpenMenu] = useState<string | null>(null);
2025-10-15 22:30:49 +08:00
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
2025-10-15 18:24:13 +08:00
const menuRef = useRef<HTMLDivElement>(null);
2025-10-15 22:30:49 +08:00
const updateMenuItems = () => {
if (uiRegistry && pluginManager) {
const items = uiRegistry.getChildMenus('window');
// 过滤掉被禁用插件的菜单项
const enabledPlugins = pluginManager.getAllPluginMetadata()
.filter(p => p.enabled)
.map(p => p.name);
// 只显示启用插件的菜单项
const filteredItems = items.filter(item => {
// 检查菜单项是否属于某个插件
return enabledPlugins.some(pluginName => {
const plugin = pluginManager.getEditorPlugin(pluginName);
if (plugin && plugin.registerMenuItems) {
const pluginMenus = plugin.registerMenuItems();
return pluginMenus.some(m => m.id === item.id);
}
return false;
});
});
setPluginMenuItems(filteredItems);
console.log('[MenuBar] Updated menu items:', filteredItems);
} else if (uiRegistry) {
// 如果没有 pluginManager显示所有菜单项
const items = uiRegistry.getChildMenus('window');
setPluginMenuItems(items);
console.log('[MenuBar] Updated menu items (no filter):', items);
}
};
useEffect(() => {
updateMenuItems();
}, [uiRegistry, pluginManager]);
useEffect(() => {
if (messageHub) {
const unsubscribeInstalled = messageHub.subscribe('plugin:installed', () => {
console.log('[MenuBar] Plugin installed, updating menu items');
updateMenuItems();
});
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
console.log('[MenuBar] Plugin enabled, updating menu items');
updateMenuItems();
});
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
console.log('[MenuBar] Plugin disabled, updating menu items');
updateMenuItems();
});
return () => {
unsubscribeInstalled();
unsubscribeEnabled();
unsubscribeDisabled();
};
}
}, [messageHub, uiRegistry, pluginManager]);
2025-10-15 18:24:13 +08:00
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
en: {
file: 'File',
newScene: 'New Scene',
openScene: 'Open Scene',
saveScene: 'Save Scene',
saveSceneAs: 'Save Scene As...',
openProject: 'Open Project',
closeProject: 'Close Project',
exit: 'Exit',
edit: 'Edit',
undo: 'Undo',
redo: 'Redo',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
delete: 'Delete',
selectAll: 'Select All',
window: 'Window',
sceneHierarchy: 'Scene Hierarchy',
inspector: 'Inspector',
assets: 'Assets',
console: 'Console',
viewport: 'Viewport',
2025-10-15 20:10:52 +08:00
pluginManager: 'Plugin Manager',
2025-10-15 22:30:49 +08:00
tools: 'Tools',
portManager: 'Port Manager',
2025-10-16 13:07:19 +08:00
settings: 'Settings',
2025-10-15 18:24:13 +08:00
help: 'Help',
documentation: 'Documentation',
2025-10-15 20:23:55 +08:00
about: 'About',
devtools: 'Developer Tools'
2025-10-15 18:24:13 +08:00
},
zh: {
file: '文件',
newScene: '新建场景',
openScene: '打开场景',
saveScene: '保存场景',
saveSceneAs: '场景另存为...',
openProject: '打开项目',
closeProject: '关闭项目',
exit: '退出',
edit: '编辑',
undo: '撤销',
redo: '重做',
cut: '剪切',
copy: '复制',
paste: '粘贴',
delete: '删除',
selectAll: '全选',
window: '窗口',
sceneHierarchy: '场景层级',
inspector: '检视器',
assets: '资产',
console: '控制台',
viewport: '视口',
2025-10-15 20:10:52 +08:00
pluginManager: '插件管理器',
2025-10-15 22:30:49 +08:00
tools: '工具',
portManager: '端口管理器',
2025-10-16 13:07:19 +08:00
settings: '设置',
2025-10-15 18:24:13 +08:00
help: '帮助',
documentation: '文档',
2025-10-15 20:23:55 +08:00
about: '关于',
devtools: '开发者工具'
2025-10-15 18:24:13 +08:00
}
};
return translations[locale]?.[key] || key;
};
const menus: Record<string, MenuItem[]> = {
file: [
{ label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
{ separator: true },
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
{ separator: true },
{ label: t('openProject'), onClick: onOpenProject },
{ label: t('closeProject'), onClick: onCloseProject },
{ separator: true },
{ label: t('exit'), onClick: onExit }
],
edit: [
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
{ separator: true },
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
{ label: t('delete'), shortcut: 'Delete', disabled: true },
{ separator: true },
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
],
window: [
{ label: t('sceneHierarchy'), disabled: true },
{ label: t('inspector'), disabled: true },
{ label: t('assets'), disabled: true },
{ label: t('console'), disabled: true },
2025-10-15 20:10:52 +08:00
{ label: t('viewport'), disabled: true },
{ separator: true },
2025-10-15 22:30:49 +08:00
...pluginMenuItems.map(item => ({
label: item.label,
shortcut: item.shortcut,
disabled: item.disabled,
onClick: item.onClick
})),
...(pluginMenuItems.length > 0 ? [{ separator: true }] : []),
2025-10-15 20:23:55 +08:00
{ label: t('pluginManager'), onClick: onOpenPluginManager },
{ separator: true },
{ label: t('devtools'), onClick: onToggleDevtools }
2025-10-15 18:24:13 +08:00
],
2025-10-15 22:30:49 +08:00
tools: [
2025-10-16 13:07:19 +08:00
{ label: t('portManager'), onClick: onOpenPortManager },
{ separator: true },
{ label: t('settings'), onClick: onOpenSettings }
2025-10-15 22:30:49 +08:00
],
2025-10-15 18:24:13 +08:00
help: [
{ label: t('documentation'), disabled: true },
{ separator: true },
{ label: t('about'), disabled: true }
]
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenu(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleMenuClick = (menuKey: string) => {
setOpenMenu(openMenu === menuKey ? null : menuKey);
};
const handleMenuItemClick = (item: MenuItem) => {
if (!item.disabled && !item.separator && item.onClick) {
item.onClick();
setOpenMenu(null);
}
};
return (
<div className="menu-bar" ref={menuRef}>
{Object.keys(menus).map(menuKey => (
<div key={menuKey} className="menu-item">
<button
className={`menu-button ${openMenu === menuKey ? 'active' : ''}`}
onClick={() => handleMenuClick(menuKey)}
>
{t(menuKey)}
</button>
{openMenu === menuKey && (
<div className="menu-dropdown">
{menus[menuKey].map((item, index) => {
if (item.separator) {
return <div key={index} className="menu-separator" />;
}
return (
<button
key={index}
className={`menu-dropdown-item ${item.disabled ? 'disabled' : ''}`}
onClick={() => handleMenuItemClick(item)}
disabled={item.disabled}
>
<span>{item.label}</span>
{item.shortcut && <span className="menu-shortcut">{item.shortcut}</span>}
</button>
);
})}
</div>
)}
</div>
))}
</div>
);
}