""" 资源管理器模块 - ImGui版本 提供文件浏览、图标显示、右键菜单等功能 """ import os import shutil 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): """初始化文件图标映射(使用PNG图标文件)""" self.icon_map = { # 3D模型文件 '.fbx': 'model', # FBX模型文件 '.obj': 'model', # OBJ模型文件 '.gltf': 'model', # glTF模型 '.glb': 'model', # glTF二进制模型 '.bam': 'model', # BAM模型文件 # 图像文件 '.jpg': 'image', # JPEG图像 '.jpeg': 'image', # JPEG图像 '.png': 'image', # PNG图像 '.bmp': 'image', # BMP图像 '.tga': 'image', # TGA图像 '.tif': 'image', # TIFF图像 '.tiff': 'image', # TIFF图像 '.hdr': 'image', # HDR图像 '.exr': 'image', # EXR图像 # 音频文件 '.mp3': 'audio', # MP3音频 '.wav': 'audio', # WAV音频 '.ogg': 'audio', # OGG音频 '.flac': 'audio', # FLAC音频 # 视频文件 '.mp4': 'video', # MP4视频 '.avi': 'video', # AVI视频 '.mov': 'video', # MOV视频 '.mkv': 'video', # MKV视频 # 文档文件 '.txt': 'document', # 文本文件 '.md': 'document', # Markdown文件 '.pdf': 'document', # PDF文件 '.doc': 'document', # Word文档 '.docx': 'document', # Word文档 '.rtf': 'document', # RTF文档 # 代码文件 '.py': 'python', # Python文件 '.js': 'code', # JavaScript文件 '.ts': 'code', # TypeScript文件 '.cpp': 'code', # C++文件 '.c': 'code', # C文件 '.h': 'code', # 头文件 '.java': 'code', # Java文件 '.cs': 'code', # C#文件 '.html': 'code', # HTML文件 '.css': 'code', # CSS文件 '.xml': 'code', # XML文件 '.json': 'config', # JSON文件 '.yaml': 'config', # YAML文件 '.yml': 'config', # YAML文件 # 配置文件 '.ini': 'config', # 配置文件 '.cfg': 'config', # 配置文件 '.conf': 'config', # 配置文件 '.toml': 'config', # 配置文件 # 压缩文件 '.zip': 'archive', # ZIP压缩包 '.rar': 'archive', # RAR压缩包 '.7z': 'archive', # 7Z压缩包 '.tar': 'archive', # TAR压缩包 '.gz': 'archive', # GZ压缩包 # 字体文件 '.ttf': 'font', # TrueType字体 '.otf': 'font', # OpenType字体 '.woff': 'font', # WOFF字体 '.woff2': 'font', # WOFF2字体 # 文件夹图标 'folder': 'folder', # 文件夹图标 'folder_open': 'folder', # 打开的文件夹图标 # 默认图标 'default': 'file', # 默认文件图标 } 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 _normalize_target_dir(self, target_dir: Optional[Path]) -> Path: """归一化目标目录,不合法时回退到当前目录。""" candidate = Path(target_dir) if target_dir else self.current_path if candidate.exists() and candidate.is_dir(): return candidate.resolve() if self.current_path.exists() and self.current_path.is_dir(): return self.current_path.resolve() return self.project_root.resolve() def _get_unique_destination_path(self, target_dir: Path, source_name: str) -> Path: """生成不冲突的目标路径。""" source_path = Path(source_name) stem = source_path.stem or source_path.name or "item" suffix = source_path.suffix candidate = target_dir / source_path.name if not candidate.exists(): return candidate index = 1 while True: if suffix: unique_name = f"{stem}_{index}{suffix}" else: unique_name = f"{stem}_{index}" candidate = target_dir / unique_name if not candidate.exists(): return candidate index += 1 def import_external_files(self, file_paths: List[Path], target_dir: Optional[Path] = None): """将外部文件导入资源目录(复制),返回(成功列表, 错误列表)。""" destination_root = self._normalize_target_dir(target_dir) destination_root.mkdir(parents=True, exist_ok=True) imported = [] errors = [] for path in file_paths: src = Path(path) if not src.exists(): errors.append(f"文件不存在: {src}") continue dst = self._get_unique_destination_path(destination_root, src.name) try: if src.is_dir(): shutil.copytree(src, dst) else: shutil.copy2(src, dst) imported.append(dst) except Exception as e: errors.append(f"导入失败 {src}: {e}") if imported: self.force_refresh() return imported, errors def move_files_to_directory(self, file_paths: List[Path], target_dir: Optional[Path]): """资源管理器内部移动文件/文件夹,返回(成功列表, 错误列表)。""" destination_root = self._normalize_target_dir(target_dir) destination_root.mkdir(parents=True, exist_ok=True) moved = [] errors = [] moved_map = {} destination_root_resolved = destination_root.resolve() for path in file_paths: src = Path(path) if not src.exists(): errors.append(f"源文件不存在: {src}") continue try: src_resolved = src.resolve() except Exception: src_resolved = src if src_resolved == destination_root_resolved: continue if src_resolved.parent == destination_root_resolved: continue if src.is_dir(): try: if destination_root_resolved.is_relative_to(src_resolved): errors.append(f"不能将目录移动到其子目录中: {src}") continue except Exception: # Python 兼容保护:is_relative_to 不可用时跳过该校验 pass dst = self._get_unique_destination_path(destination_root, src.name) try: shutil.move(str(src), str(dst)) moved.append(dst) moved_map[src_resolved] = dst.resolve() except Exception as e: errors.append(f"移动失败 {src}: {e}") if moved: updated_selection = set() for selected in self.selected_files: try: selected_resolved = selected.resolve() except Exception: selected_resolved = selected if selected_resolved in moved_map: updated_selection.add(moved_map[selected_resolved]) elif selected.exists(): updated_selection.add(selected) self.selected_files = updated_selection if self.focused_file: try: focused_resolved = self.focused_file.resolve() except Exception: focused_resolved = self.focused_file if focused_resolved in moved_map: self.focused_file = moved_map[focused_resolved] self.force_refresh() return moved, errors def start_drag(self, files: List[Path]): """开始拖拽操作""" self.dragged_files = [Path(f) for f in files if Path(f).exists()] 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