373 lines
14 KiB
Python
373 lines
14 KiB
Python
"""
|
||
资源管理器模块 - ImGui版本
|
||
提供文件浏览、图标显示、右键菜单等功能
|
||
"""
|
||
|
||
import os
|
||
import subprocess
|
||
import platform
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Tuple, Set
|
||
import time
|
||
|
||
|
||
class ResourceManager:
|
||
"""ImGui资源管理器类"""
|
||
|
||
def __init__(self, world):
|
||
self.world = world
|
||
self.project_root = Path(__file__).resolve().parent.parent
|
||
|
||
# 当前浏览路径,默认从Resources目录开始
|
||
self.current_path = self.project_root / "Resources"
|
||
if not self.current_path.exists():
|
||
self.current_path = self.project_root
|
||
|
||
# 历史记录,用于前进后退导航
|
||
self.navigation_history: List[Path] = [self.current_path]
|
||
self.history_index = 0
|
||
|
||
# 文件选择状态
|
||
self.selected_files: Set[Path] = set()
|
||
self.focused_file: Optional[Path] = None
|
||
|
||
# 右键菜单状态
|
||
self.show_context_menu = False
|
||
self.context_menu_file: Optional[Path] = None
|
||
self.context_menu_position = (0, 0)
|
||
|
||
# 搜索和过滤
|
||
self.search_filter = ""
|
||
self.show_hidden_files = False
|
||
|
||
# 文件系统监控
|
||
self.last_refresh_time = 0
|
||
self.refresh_interval = 1.0 # 秒
|
||
self.auto_refresh_enabled = True
|
||
self.last_directory_content = {} # 缓存目录内容,用于检测变化
|
||
|
||
# 拖拽状态
|
||
self.dragged_files: List[Path] = []
|
||
|
||
# 展开/折叠状态
|
||
self.expanded_directories: Set[Path] = set()
|
||
|
||
# 文件图标映射(Unicode Emoji)
|
||
self._init_icon_map()
|
||
|
||
def _init_icon_map(self):
|
||
"""初始化文件图标映射"""
|
||
self.icon_map = {
|
||
# 编程语言文件
|
||
'.py': '🐍', # Python文件
|
||
'.js': '⚡', # JavaScript文件
|
||
'.ts': '⚡', # TypeScript文件
|
||
'.html': '🌐', # HTML文件
|
||
'.css': '🎨', # CSS文件
|
||
'.json': '📋', # JSON文件
|
||
'.xml': '📄', # XML文件
|
||
'.yaml': '📄', # YAML文件
|
||
'.yml': '📄', # YAML文件
|
||
'.md': '📝', # Markdown文档
|
||
|
||
# 3D模型文件
|
||
'.fbx': '🎭', # FBX模型文件
|
||
'.obj': '🎭', # OBJ模型文件
|
||
'.gltf': '🎭', # glTF模型
|
||
'.glb': '🎭', # glTF二进制模型
|
||
'.bam': '🎭', # BAM模型文件
|
||
'.egg': '🎭', # EGG模型文件
|
||
'.dae': '🎭', # Collada模型
|
||
'.3ds': '🎭', # 3DS模型
|
||
'.blend': '🎭', # Blender文件
|
||
|
||
# 图像文件
|
||
'.jpg': '🖼️', # JPEG图像
|
||
'.jpeg': '🖼️', # JPEG图像
|
||
'.png': '🖼️', # PNG图像
|
||
'.gif': '🖼️', # GIF图像
|
||
'.bmp': '🖼️', # BMP图像
|
||
'.tga': '🖼️', # TGA图像
|
||
'.tiff': '🖼️', # TIFF图像
|
||
'.webp': '🖼️', # WebP图像
|
||
'.svg': '🖼️', # SVG图像
|
||
'.ico': '🖼️', # ICO图标
|
||
|
||
# 音频文件
|
||
'.mp3': '🎵', # MP3音频
|
||
'.wav': '🎵', # WAV音频
|
||
'.ogg': '🎵', # OGG音频
|
||
'.flac': '🎵', # FLAC音频
|
||
'.m4a': '🎵', # M4A音频
|
||
|
||
# 视频文件
|
||
'.mp4': '🎬', # MP4视频
|
||
'.avi': '🎬', # AVI视频
|
||
'.mkv': '🎬', # MKV视频
|
||
'.mov': '🎬', # MOV视频
|
||
'.wmv': '🎬', # WMV视频
|
||
'.flv': '🎬', # FLV视频
|
||
|
||
# 文档文件
|
||
'.txt': '📄', # 纯文本文件
|
||
'.pdf': '📕', # PDF文档
|
||
'.doc': '📘', # Word文档
|
||
'.docx': '📘', # Word文档
|
||
'.xls': '📗', # Excel表格
|
||
'.xlsx': '📗', # Excel表格
|
||
'.ppt': '📙', # PowerPoint演示
|
||
'.pptx': '📙', # PowerPoint演示
|
||
|
||
# 压缩文件
|
||
'.zip': '📦', # ZIP压缩包
|
||
'.rar': '📦', # RAR压缩包
|
||
'.7z': '📦', # 7Z压缩包
|
||
'.tar': '📦', # TAR压缩包
|
||
'.gz': '📦', # GZ压缩包
|
||
|
||
# 配置文件
|
||
'.ini': '⚙️', # INI配置文件
|
||
'.cfg': '⚙️', # CFG配置文件
|
||
'.conf': '⚙️', # CONF配置文件
|
||
'.toml': '⚙️', # TOML配置文件
|
||
|
||
# 字体文件
|
||
'.ttf': '🔤', # TrueType字体
|
||
'.otf': '🔤', # OpenType字体
|
||
'.woff': '🔤', # WOFF字体
|
||
'.woff2': '🔤', # WOFF2字体
|
||
|
||
# 默认图标
|
||
'default': '📄', # 默认文件图标
|
||
'folder': '📁', # 文件夹图标
|
||
'folder_open': '📂', # 打开的文件夹图标
|
||
'drive': '💾', # 驱动器图标
|
||
'home': '🏠', # 主目录图标
|
||
}
|
||
|
||
def get_file_icon(self, filename: str, is_folder: bool = False) -> str:
|
||
"""根据文件名获取图标"""
|
||
if is_folder:
|
||
return self.icon_map['folder']
|
||
|
||
ext = Path(filename).suffix.lower()
|
||
return self.icon_map.get(ext, self.icon_map['default'])
|
||
|
||
def get_directory_contents(self, path: Path) -> Tuple[List[Path], List[Path]]:
|
||
"""获取目录内容,返回(目录列表, 文件列表)"""
|
||
if not path.exists() or not path.is_dir():
|
||
return [], []
|
||
|
||
dirs = []
|
||
files = []
|
||
|
||
try:
|
||
for item in path.iterdir():
|
||
# 跳过隐藏文件(除非启用显示隐藏文件)
|
||
if not self.show_hidden_files and item.name.startswith('.'):
|
||
continue
|
||
|
||
if item.is_dir():
|
||
dirs.append(item)
|
||
else:
|
||
files.append(item)
|
||
|
||
# 排序:目录和文件分别按名称排序
|
||
dirs.sort(key=lambda x: x.name.lower())
|
||
files.sort(key=lambda x: x.name.lower())
|
||
|
||
except PermissionError:
|
||
pass
|
||
|
||
return dirs, files
|
||
|
||
def navigate_to(self, path: Path):
|
||
"""导航到指定路径"""
|
||
if path.exists() and path.is_dir():
|
||
self.current_path = path.resolve()
|
||
|
||
# 更新导航历史
|
||
if self.history_index < len(self.navigation_history) - 1:
|
||
# 如果不在历史末尾,截断后面的历史
|
||
self.navigation_history = self.navigation_history[:self.history_index + 1]
|
||
|
||
self.navigation_history.append(self.current_path)
|
||
self.history_index += 1
|
||
|
||
# 清除选择状态
|
||
self.selected_files.clear()
|
||
self.focused_file = None
|
||
|
||
def navigate_back(self):
|
||
"""后退到上一个目录"""
|
||
if self.history_index > 0:
|
||
self.history_index -= 1
|
||
self.current_path = self.navigation_history[self.history_index]
|
||
self.selected_files.clear()
|
||
self.focused_file = None
|
||
|
||
def navigate_forward(self):
|
||
"""前进到下一个目录"""
|
||
if self.history_index < len(self.navigation_history) - 1:
|
||
self.history_index += 1
|
||
self.current_path = self.navigation_history[self.history_index]
|
||
self.selected_files.clear()
|
||
self.focused_file = None
|
||
|
||
def navigate_up(self):
|
||
"""导航到父目录"""
|
||
parent = self.current_path.parent
|
||
if parent != self.current_path: # 避免在根目录时循环
|
||
self.navigate_to(parent)
|
||
|
||
def navigate_to_selected(self):
|
||
"""导航到选中的目录"""
|
||
if self.focused_file and self.focused_file.is_dir():
|
||
self.navigate_to(self.focused_file)
|
||
|
||
def open_file(self, file_path: Path):
|
||
"""使用系统默认程序打开文件"""
|
||
try:
|
||
if platform.system() == "Windows":
|
||
os.startfile(str(file_path))
|
||
elif platform.system() == "Darwin": # macOS
|
||
subprocess.run(["open", str(file_path)], check=True)
|
||
else: # Linux
|
||
subprocess.run(["xdg-open", str(file_path)], check=True)
|
||
except Exception as e:
|
||
print(f"无法打开文件 {file_path}: {e}")
|
||
|
||
def get_file_size_string(self, path: Path) -> str:
|
||
"""获取文件大小的字符串表示"""
|
||
if path.is_dir():
|
||
return ""
|
||
|
||
try:
|
||
size = path.stat().st_size
|
||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||
if size < 1024.0:
|
||
return f"{size:.1f} {unit}"
|
||
size /= 1024.0
|
||
return f"{size:.1f} TB"
|
||
except:
|
||
return ""
|
||
|
||
def should_show_file(self, path: Path) -> bool:
|
||
"""判断文件是否应该显示(基于搜索过滤)"""
|
||
if not self.search_filter:
|
||
return True
|
||
|
||
return self.search_filter.lower() in path.name.lower()
|
||
|
||
def toggle_directory_expansion(self, path: Path):
|
||
"""切换目录展开/折叠状态"""
|
||
if path in self.expanded_directories:
|
||
self.expanded_directories.remove(path)
|
||
else:
|
||
self.expanded_directories.add(path)
|
||
|
||
def is_directory_expanded(self, path: Path) -> bool:
|
||
"""检查目录是否展开"""
|
||
return path in self.expanded_directories
|
||
|
||
def refresh_if_needed(self):
|
||
"""如果需要则刷新目录内容"""
|
||
if not self.auto_refresh_enabled:
|
||
return False
|
||
|
||
current_time = time.time()
|
||
if current_time - self.last_refresh_time > self.refresh_interval:
|
||
self.last_refresh_time = current_time
|
||
|
||
# 检查目录内容是否发生变化
|
||
if self._has_directory_changed():
|
||
return True
|
||
return False
|
||
|
||
def _has_directory_changed(self) -> bool:
|
||
"""检查目录内容是否发生变化"""
|
||
try:
|
||
# 获取当前目录内容
|
||
dirs, files = self.get_directory_contents(self.current_path)
|
||
current_content = {
|
||
'dirs': {d.name: d.stat().st_mtime for d in dirs},
|
||
'files': {f.name: f.stat().st_mtime for f in files}
|
||
}
|
||
|
||
# 获取缓存的内容
|
||
cache_key = str(self.current_path)
|
||
cached_content = self.last_directory_content.get(cache_key)
|
||
|
||
if cached_content is None:
|
||
# 首次访问,缓存内容
|
||
self.last_directory_content[cache_key] = current_content
|
||
return False
|
||
|
||
# 比较内容
|
||
if cached_content != current_content:
|
||
# 内容发生变化,更新缓存
|
||
self.last_directory_content[cache_key] = current_content
|
||
return True
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"检查目录变化时出错: {e}")
|
||
return False
|
||
|
||
def force_refresh(self):
|
||
"""强制刷新当前目录"""
|
||
cache_key = str(self.current_path)
|
||
if cache_key in self.last_directory_content:
|
||
del self.last_directory_content[cache_key]
|
||
self.last_refresh_time = 0
|
||
|
||
def set_auto_refresh(self, enabled: bool):
|
||
"""设置自动刷新开关"""
|
||
self.auto_refresh_enabled = enabled
|
||
if enabled:
|
||
self.force_refresh()
|
||
|
||
def get_project_root(self) -> Path:
|
||
"""获取项目根目录"""
|
||
current = Path(__file__).resolve()
|
||
while current.parent != current:
|
||
if (current / "main.py").exists() or (current / ".git").exists():
|
||
return current
|
||
current = current.parent
|
||
return Path(__file__).resolve().parent.parent
|
||
|
||
def get_relative_path(self, path: Path) -> str:
|
||
"""获取相对于项目根目录的路径"""
|
||
try:
|
||
return str(path.relative_to(self.project_root))
|
||
except ValueError:
|
||
return str(path)
|
||
|
||
def start_drag(self, files: List[Path]):
|
||
"""开始拖拽操作"""
|
||
self.dragged_files = files.copy()
|
||
|
||
def clear_drag(self):
|
||
"""清除拖拽状态"""
|
||
self.dragged_files.clear()
|
||
|
||
def is_dragging(self) -> bool:
|
||
"""检查是否正在拖拽"""
|
||
return len(self.dragged_files) > 0
|
||
|
||
def get_dragged_files(self) -> List[Path]:
|
||
"""获取当前拖拽的文件列表"""
|
||
return self.dragged_files.copy()
|
||
|
||
def can_drag_to_scene(self) -> bool:
|
||
"""检查是否可以拖拽到场景中"""
|
||
if not self.is_dragging():
|
||
return False
|
||
|
||
# 检查是否有支持的3D模型文件
|
||
supported_extensions = {'.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'}
|
||
for file_path in self.dragged_files:
|
||
if file_path.suffix.lower() in supported_extensions:
|
||
return True
|
||
return False |