diff --git a/core/drag_drop/__init__.py b/core/drag_drop/__init__.py new file mode 100644 index 00000000..74ea592b --- /dev/null +++ b/core/drag_drop/__init__.py @@ -0,0 +1,10 @@ +""" +跨平台拖拽检测模块 + +提供统一的拖拽检测接口,支持Windows、Linux和macOS平台。 +""" + +from .base_detector import BaseDragDetector +from .platform_detector import PlatformDragDetector + +__all__ = ['BaseDragDetector', 'PlatformDragDetector'] \ No newline at end of file diff --git a/core/drag_drop/base_detector.py b/core/drag_drop/base_detector.py new file mode 100644 index 00000000..07d638b3 --- /dev/null +++ b/core/drag_drop/base_detector.py @@ -0,0 +1,120 @@ +""" +拖拽检测器基类 + +定义所有平台拖拽检测器的通用接口。 +""" + +import os +import platform +from abc import ABC, abstractmethod +from typing import List, Optional, Callable +import threading +import queue +import time + + +class BaseDragDetector(ABC): + """拖拽检测器基类""" + + def __init__(self, supported_formats: List[str] = None): + """ + 初始化拖拽检测器 + + Args: + supported_formats: 支持的文件格式列表,如 ['.gltf', '.glb', '.fbx'] + """ + self.supported_formats = supported_formats or ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'] + self.is_running = False + self.monitor_thread = None + self.file_queue = queue.Queue() + self.drop_callback: Optional[Callable[[List[str]], None]] = None + + def set_drop_callback(self, callback: Callable[[List[str]], None]): + """ + 设置文件拖拽回调函数 + + Args: + callback: 接收文件路径列表的回调函数 + """ + self.drop_callback = callback + + def start_monitoring(self): + """开始监控拖拽事件""" + if not self.is_running: + self.is_running = True + self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) + self.monitor_thread.start() + + def stop_monitoring(self): + """停止监控拖拽事件""" + self.is_running = False + if self.monitor_thread: + self.monitor_thread.join(timeout=1.0) + + def get_dropped_files(self) -> List[str]: + """获取拖拽的文件列表""" + files = [] + while not self.file_queue.empty(): + try: + files.append(self.file_queue.get_nowait()) + except queue.Empty: + break + return files + + def add_dropped_file(self, file_path: str): + """ + 添加拖拽的文件路径 + + Args: + file_path: 拖拽的文件路径 + """ + if self._is_supported_format(file_path): + self.file_queue.put(file_path) + if self.drop_callback: + self.drop_callback([file_path]) + + def _is_supported_format(self, file_path: str) -> bool: + """检查文件格式是否支持""" + _, ext = os.path.splitext(file_path.lower()) + return ext in self.supported_formats + + @abstractmethod + def _monitor_loop(self): + """监控循环,由子类实现""" + pass + + @abstractmethod + def is_supported(self) -> bool: + """检查当前平台是否支持此检测器""" + pass + + +class DragDetectorFactory: + """拖拽检测器工厂类""" + + @staticmethod + def create_detector(supported_formats: List[str] = None) -> BaseDragDetector: + """ + 根据当前平台创建合适的拖拽检测器 + + Args: + supported_formats: 支持的文件格式列表 + + Returns: + 适合当前平台的拖拽检测器实例 + """ + system = platform.system().lower() + + if system == 'windows': + from .windows_detector import WindowsDragDetector + return WindowsDragDetector(supported_formats) + elif system == 'linux': + from .linux_detector import LinuxDragDetector + return LinuxDragDetector(supported_formats) + elif system == 'darwin': # macOS + from .macos_detector import MacOSDragDetector + return MacOSDragDetector(supported_formats) + else: + # 降级到基础检测器 + from .fallback_detector import FallbackDragDetector + return FallbackDragDetector(supported_formats) \ No newline at end of file diff --git a/core/drag_drop/fallback_detector.py b/core/drag_drop/fallback_detector.py new file mode 100644 index 00000000..980b9015 --- /dev/null +++ b/core/drag_drop/fallback_detector.py @@ -0,0 +1,83 @@ +""" +降级拖拽检测器 + +当平台特定检测器不可用时的降级方案。 +""" + +import os +import time +import threading +from typing import List +from .base_detector import BaseDragDetector + + +class FallbackDragDetector(BaseDragDetector): + """降级拖拽检测器""" + + def __init__(self, supported_formats: List[str] = None): + """ + 初始化降级检测器 + + Args: + supported_formats: 支持的文件格式列表 + """ + super().__init__(supported_formats) + self._watch_directory = os.path.expanduser("~/Desktop") # 监控桌面 + self._known_files = set() + self._scan_interval = 0.5 # 扫描间隔(秒) + + # 初始化已知文件列表 + self._scan_known_files() + + def _scan_known_files(self): + """扫描已知文件""" + self._known_files.clear() + if os.path.exists(self._watch_directory): + for filename in os.listdir(self._watch_directory): + if self._is_supported_format(filename): + filepath = os.path.join(self._watch_directory, filename) + self._known_files.add(filepath) + + def _monitor_loop(self): + """监控循环 - 通过文件系统变化检测""" + while self.is_running: + try: + if os.path.exists(self._watch_directory): + current_files = set() + for filename in os.listdir(self._watch_directory): + if self._is_supported_format(filename): + filepath = os.path.join(self._watch_directory, filename) + current_files.add(filepath) + + # 检测新文件 + new_files = current_files - self._known_files + for filepath in new_files: + # 检查文件是否最近创建(1秒内) + try: + file_time = os.path.getmtime(filepath) + if time.time() - file_time < 1.0: + self.add_dropped_file(filepath) + except OSError: + pass + + self._known_files = current_files + + except Exception as e: + print(f"降级检测器错误: {e}") + + time.sleep(self._scan_interval) + + def is_supported(self) -> bool: + """降级检测器总是支持""" + return True + + def set_watch_directory(self, directory: str): + """ + 设置监控目录 + + Args: + directory: 要监控的目录路径 + """ + if os.path.exists(directory): + self._watch_directory = directory + self._scan_known_files() \ No newline at end of file diff --git a/core/drag_drop/linux_detector.py b/core/drag_drop/linux_detector.py new file mode 100644 index 00000000..cb0162a7 --- /dev/null +++ b/core/drag_drop/linux_detector.py @@ -0,0 +1,395 @@ +""" +Linux平台拖拽检测器 + +支持X11和Wayland窗口系统的拖拽检测。 +""" + +import os +import sys +import time +import threading +import subprocess +from typing import List, Optional +from .base_detector import BaseDragDetector + + +class LinuxDragDetector(BaseDragDetector): + """Linux平台拖拽检测器""" + + def __init__(self, supported_formats: List[str] = None): + """ + 初始化Linux拖拽检测器 + + Args: + supported_formats: 支持的文件格式列表 + """ + super().__init__(supported_formats) + self._window_id = None + self._display = None + self._is_x11 = self._check_x11() + self._is_wayland = self._check_wayland() + self._xdg_active = self._check_xdg() + + def _check_x11(self) -> bool: + """检查是否使用X11""" + try: + # 检查DISPLAY环境变量 + if os.environ.get('DISPLAY'): + # 尝试连接X11显示 + result = subprocess.run(['xset', 'q'], + capture_output=True, + timeout=2) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return False + + def _check_wayland(self) -> bool: + """检查是否使用Wayland""" + return os.environ.get('WAYLAND_DISPLAY') is not None + + def _check_xdg(self) -> bool: + """检查XDG桌面环境是否可用""" + try: + # 检查是否安装了xdg-user-dir + result = subprocess.run(['which', 'xdg-user-dir'], + capture_output=True) + return result.returncode == 0 + except FileNotFoundError: + return False + + def _get_window_id(self) -> Optional[str]: + """获取当前窗口ID""" + try: + # 使用xdotool获取活动窗口 + result = subprocess.run(['xdotool', 'getactivewindow'], + capture_output=True, + text=True, + timeout=2) + if result.returncode == 0: + return result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # 备用方法:使用wmctrl + try: + result = subprocess.run(['wmctrl', '-l'], + capture_output=True, + text=True, + timeout=2) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + if lines: + # 获取第一个窗口的ID + return lines[0].split()[0] + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + return None + + def _monitor_x11_drag(self): + """监控X11拖拽事件""" + try: + # 使用xev监控窗口事件 + window_id = self._get_window_id() + if not window_id: + print("无法获取窗口ID,切换到文件监控") + self._monitor_file_drag() + return + + cmd = ['xev', '-id', window_id, '-event', 'pointer'] + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True) + + print(f"开始监控X11窗口 {window_id} 的拖拽事件") + + while self.is_running: + line = process.stdout.readline() + if not line: + break + + # 检测拖拽相关事件 + if any(keyword in line for keyword in ['DND', 'Xdnd', 'ClientMessage', 'SelectionNotify']): + # 尝试获取拖拽的文件 + self._extract_x11_drag_files() + + except Exception as e: + print(f"X11拖拽监控错误: {e}") + print("切换到文件监控模式") + self._monitor_file_drag() + finally: + if 'process' in locals(): + process.terminate() + + def _extract_x11_drag_files(self): + """从X11拖拽事件中提取文件路径""" + try: + # 使用xclip获取剪贴板内容(可能包含拖拽的文件) + result = subprocess.run(['xclip', '-selection', 'primary', '-o'], + capture_output=True, + text=True, + timeout=1) + if result.returncode == 0 and result.stdout.strip(): + self._process_clipboard_content(result.stdout.strip()) + + except (subprocess.TimeoutExpired, FileNotFoundError): + # 尝试使用xsel作为备选 + try: + result = subprocess.run(['xsel', '--primary', '--output'], + capture_output=True, + text=True, + timeout=1) + if result.returncode == 0 and result.stdout.strip(): + self._process_clipboard_content(result.stdout.strip()) + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + def _process_clipboard_content(self, content): + """处理剪贴板内容,提取文件路径""" + # 检查是否包含文件路径 + lines = content.split('\n') + for line in lines: + line = line.strip() + if line.startswith('file://'): + # URI格式 + file_path = line[7:] # 移除 'file://' 前缀 + file_path = file_path.replace('%20', ' ') # 解码空格 + if self._is_supported_format(file_path): + self.add_dropped_file(file_path) + elif os.path.isabs(line) and self._is_supported_format(line): + # 绝对路径 + self.add_dropped_file(line) + + def _monitor_wayland_drag(self): + """监控Wayland拖拽事件""" + try: + # 尝试使用wlrctl监控拖拽事件 + if self._check_command_exists('wlrctl'): + self._monitor_wlrctl_drag() + else: + # 尝试使用wl-paste监控剪贴板 + if self._check_command_exists('wl-paste'): + self._monitor_wayland_clipboard() + else: + print("Wayland拖拽监控工具不可用,切换到文件监控") + self._monitor_file_drag() + except Exception as e: + print(f"Wayland拖拽监控错误: {e}") + print("切换到文件监控模式") + self._monitor_file_drag() + + def _check_command_exists(self, command): + """检查命令是否存在""" + try: + result = subprocess.run(['which', command], + capture_output=True, + timeout=1) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + return False + + def _monitor_wlrctl_drag(self): + """使用wlrctl监控拖拽事件""" + print("使用wlrctl监控Wayland拖拽事件") + try: + # wlrctl可能支持拖拽监控 + process = subprocess.Popen(['wlrctl', 'drag'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True) + + while self.is_running: + line = process.stdout.readline() + if not line: + break + + # 解析wlrctl输出 + if 'file://' in line or os.path.isabs(line.strip()): + self._process_drag_line(line.strip()) + + except Exception as e: + print(f"wlrctl监控失败: {e}") + raise + finally: + if 'process' in locals(): + process.terminate() + + def _monitor_wayland_clipboard(self): + """监控Wayland剪贴板变化""" + print("使用wl-paste监控Wayland剪贴板") + last_clipboard = "" + + while self.is_running: + try: + # 获取当前剪贴板内容 + result = subprocess.run(['wl-paste', '--type', 'text/uri-list'], + capture_output=True, + text=True, + timeout=1) + + if result.returncode == 0: + current_clipboard = result.stdout.strip() + if current_clipboard and current_clipboard != last_clipboard: + last_clipboard = current_clipboard + self._process_clipboard_content(current_clipboard) + + except subprocess.TimeoutExpired: + pass + except Exception as e: + print(f"Wayland剪贴板监控错误: {e}") + + time.sleep(0.5) + + def _process_drag_line(self, line): + """处理拖拽行内容""" + if line.startswith('file://'): + file_path = line[7:] + file_path = file_path.replace('%20', ' ') + if self._is_supported_format(file_path): + self.add_dropped_file(file_path) + elif os.path.isabs(line) and self._is_supported_format(line): + self.add_dropped_file(line) + + def _get_user_directories(self): + """获取用户常用目录""" + directories = [] + + # 获取桌面目录 + desktop = os.path.expanduser("~/Desktop") + if os.path.exists(desktop): + directories.append(desktop) + + # 获取下载目录 + downloads = os.path.expanduser("~/Downloads") + if os.path.exists(downloads): + directories.append(downloads) + + # 获取主目录 + home = os.path.expanduser("~") + if os.path.exists(home): + directories.append(home) + + # 获取临时目录 + temp_dirs = ['/tmp', '/var/tmp'] + for temp_dir in temp_dirs: + if os.path.exists(temp_dir): + directories.append(temp_dir) + + return directories + + def _monitor_file_drag(self): + """通过文件系统变化监控拖拽""" + watch_dirs = self._get_user_directories() + known_files = {} + recent_files = {} # 用于跟踪最近添加的文件 + + # 初始化已知文件 + for watch_dir in watch_dirs: + if os.path.exists(watch_dir): + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + if self._is_supported_format(filepath): + try: + stat = os.stat(filepath) + known_files[filepath] = stat.st_mtime + except OSError: + pass + + print(f"开始监控目录: {watch_dirs}") + print(f"已知文件数量: {len(known_files)}") + + while self.is_running: + try: + current_time = time.time() + + for watch_dir in watch_dirs: + if not os.path.exists(watch_dir): + continue + + try: + # 获取当前目录中的文件 + current_files = {} + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + if self._is_supported_format(filepath): + try: + stat = os.stat(filepath) + current_files[filepath] = stat.st_mtime + + # 检查是否是新文件(最近5秒内创建) + if filepath not in known_files: + time_diff = current_time - stat.st_mtime + if time_diff < 5.0: + # 检查文件大小是否稳定(避免检测到正在下载的文件) + if self._is_file_stable(filepath): + print(f"检测到新文件: {filepath}") + recent_files[filepath] = current_time + self.add_dropped_file(filepath) + # 检查已知文件是否被更新 + elif filepath in known_files: + if stat.st_mtime > known_files[filepath]: + time_diff = current_time - stat.st_mtime + if time_diff < 5.0 and filepath not in recent_files: + print(f"检测到文件更新: {filepath}") + recent_files[filepath] = current_time + self.add_dropped_file(filepath) + + except OSError: + continue + + # 更新已知文件列表 + known_files.update(current_files) + + except PermissionError: + print(f"无权限访问目录: {watch_dir}") + continue + except Exception as e: + print(f"监控目录 {watch_dir} 时出错: {e}") + + # 清理过期的最近文件记录(超过30秒) + expired_files = [path for path, timestamp in recent_files.items() + if current_time - timestamp > 30] + for path in expired_files: + del recent_files[path] + + except Exception as e: + print(f"文件拖拽监控错误: {e}") + + time.sleep(0.5) + + def _is_file_stable(self, filepath, check_interval=0.5, max_checks=3): + """检查文件大小是否稳定(避免检测到正在下载的文件)""" + try: + initial_size = os.path.getsize(filepath) + for i in range(max_checks): + time.sleep(check_interval) + current_size = os.path.getsize(filepath) + if current_size != initial_size: + return False + return True + except OSError: + return False + + def _monitor_loop(self): + """主监控循环""" + if self._is_x11: + try: + self._monitor_x11_drag() + except Exception as e: + print(f"X11监控失败,切换到文件监控: {e}") + self._monitor_file_drag() + elif self._is_wayland: + try: + self._monitor_wayland_drag() + except Exception as e: + print(f"Wayland监控失败,切换到文件监控: {e}") + self._monitor_file_drag() + else: + # 降级到文件监控 + self._monitor_file_drag() + + def is_supported(self) -> bool: + """检查Linux平台是否支持""" + return self._is_x11 or self._is_wayland or self._xdg_active \ No newline at end of file diff --git a/core/drag_drop/macos_detector.py b/core/drag_drop/macos_detector.py new file mode 100644 index 00000000..b8f3bdb0 --- /dev/null +++ b/core/drag_drop/macos_detector.py @@ -0,0 +1,105 @@ +""" +macOS平台拖拽检测器 + +使用macOS Cocoa API实现系统级拖拽检测。 +""" + +import os +import sys +import time +import threading +from typing import List, Optional +from .base_detector import BaseDragDetector + + +class MacOSDragDetector(BaseDragDetector): + """macOS平台拖拽检测器""" + + def __init__(self, supported_formats: List[str] = None): + """ + 初始化macOS拖拽检测器 + + Args: + supported_formats: 支持的文件格式列表 + """ + super().__init__(supported_formats) + self._app_kit_available = self._check_app_kit() + self._cocoa_available = self._check_cocoa() + + def _check_app_kit(self) -> bool: + """检查AppKit是否可用""" + try: + import AppKit + return True + except ImportError: + return False + + def _check_cocoa(self) -> bool: + """检查Cocoa是否可用""" + try: + import Cocoa + return True + except ImportError: + return False + + def _setup_cocoa_drag_drop(self) -> bool: + """设置Cocoa拖拽接收""" + try: + if not self._app_kit_available: + return False + + import AppKit + + # 创建拖拽接收器 + # 这里需要实现macOS的拖拽API调用 + # 暂时返回False,需要进一步实现 + return False + except Exception as e: + print(f"设置Cocoa拖拽失败: {e}") + return False + + def _monitor_file_drag(self): + """通过文件系统变化监控拖拽""" + # 监控用户桌面和下载目录 + desktop_path = os.path.expanduser("~/Desktop") + downloads_path = os.path.expanduser("~/Downloads") + + watch_dirs = [desktop_path, downloads_path] + + while self.is_running: + try: + current_time = time.time() + for watch_dir in watch_dirs: + if not os.path.exists(watch_dir): + continue + + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + try: + file_time = os.path.getmtime(filepath) + # 如果文件是最近1秒内创建的 + if current_time - file_time < 1.0: + if self._is_supported_format(filepath): + self.add_dropped_file(filepath) + except OSError: + continue + + except Exception as e: + print(f"文件拖拽监控错误: {e}") + + time.sleep(0.5) + + def _monitor_loop(self): + """主监控循环""" + if self._app_kit_available and self._cocoa_available: + # 尝试设置真正的拖拽接收 + if not self._setup_cocoa_drag_drop(): + # 降级到文件系统监控 + self._monitor_file_drag() + else: + # 降级到文件系统监控 + self._monitor_file_drag() + + def is_supported(self) -> bool: + """检查macOS平台是否支持""" + return sys.platform.startswith('darwin') \ No newline at end of file diff --git a/core/drag_drop/platform_detector.py b/core/drag_drop/platform_detector.py new file mode 100644 index 00000000..abb5a1a0 --- /dev/null +++ b/core/drag_drop/platform_detector.py @@ -0,0 +1,66 @@ +""" +平台拖拽检测器 + +自动选择合适的平台特定拖拽检测器。 +""" + +from .base_detector import BaseDragDetector, DragDetectorFactory +from typing import List + + +class PlatformDragDetector: + """平台拖拽检测器包装类""" + + def __init__(self, supported_formats: List[str] = None): + """ + 初始化平台拖拽检测器 + + Args: + supported_formats: 支持的文件格式列表 + """ + self.supported_formats = supported_formats or ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'] + self.detector = DragDetectorFactory.create_detector(self.supported_formats) + self._is_monitoring = False + + def set_drop_callback(self, callback): + """设置拖拽回调函数""" + if self.detector: + self.detector.set_drop_callback(callback) + + def start_monitoring(self): + """开始监控""" + if self.detector and not self._is_monitoring: + self.detector.start_monitoring() + self._is_monitoring = True + + def stop_monitoring(self): + """停止监控""" + if self.detector and self._is_monitoring: + self.detector.stop_monitoring() + self._is_monitoring = False + + def get_dropped_files(self): + """获取拖拽的文件""" + if self.detector: + return self.detector.get_dropped_files() + return [] + + def is_supported(self) -> bool: + """检查当前平台是否支持""" + return self.detector.is_supported() if self.detector else False + + def add_dropped_file(self, file_path: str): + """添加拖拽的文件路径""" + if self.detector: + self.detector.add_dropped_file(file_path) + + def get_platform_info(self) -> dict: + """获取平台信息""" + import platform + return { + 'system': platform.system(), + 'release': platform.release(), + 'version': platform.version(), + 'detector_type': type(self.detector).__name__, + 'supported': self.is_supported() + } \ No newline at end of file diff --git a/core/drag_drop/simple_detector.py b/core/drag_drop/simple_detector.py new file mode 100644 index 00000000..cc7edc15 --- /dev/null +++ b/core/drag_drop/simple_detector.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +简化的拖拽检测器 + +使用文件系统监控和剪贴板检测实现拖拽功能 +""" + +import os +import sys +import time +import subprocess +import threading +from typing import List, Optional, Callable +from collections import deque +import tempfile + +class SimpleDragDetector: + """简化的拖拽检测器""" + + def __init__(self, supported_formats: List[str]): + self.supported_formats = supported_formats + self.is_running = False + self.drop_callback = None + self.dropped_files = deque() + self.monitor_thread = None + self.last_clipboard = "" + + def set_drop_callback(self, callback: Callable[[List[str]], None]): + """设置拖拽回调函数""" + self.drop_callback = callback + + def start_monitoring(self): + """开始监控""" + if self.is_running: + return + + self.is_running = True + self.monitor_thread = threading.Thread(target=self._monitor_loop) + self.monitor_thread.daemon = True + self.monitor_thread.start() + print("✓ 简化拖拽监控已启动") + + def stop_monitoring(self): + """停止监控""" + self.is_running = False + if self.monitor_thread: + self.monitor_thread.join(timeout=1) + + def get_dropped_files(self) -> List[str]: + """获取拖拽的文件列表""" + files = list(self.dropped_files) + self.dropped_files.clear() + return files + + def add_dropped_file(self, file_path: str): + """添加拖拽的文件""" + if os.path.exists(file_path): + file_ext = os.path.splitext(file_path)[1].lower() + if file_ext in self.supported_formats: + self.dropped_files.append(file_path) + print(f"检测到拖拽文件: {file_path}") + + def _monitor_loop(self): + """监控循环""" + watch_dirs = self._get_watch_directories() + known_files = set() + + # 初始化已知文件 + for watch_dir in watch_dirs: + self._scan_directory(watch_dir, known_files) + + print(f"监控目录: {watch_dirs}") + print(f"已知文件数量: {len(known_files)}") + + while self.is_running: + try: + current_time = time.time() + + # 1. 检查剪贴板变化 + self._check_clipboard() + + # 2. 检查文件系统变化 + for watch_dir in watch_dirs: + if not os.path.exists(watch_dir): + continue + + try: + current_files = set() + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + if self._is_supported_format(filepath): + try: + stat = os.stat(filepath) + current_files.add(filepath) + + # 检查新文件 + if filepath not in known_files: + time_diff = current_time - stat.st_mtime + if time_diff < 5.0: # 5秒内创建的文件 + if self._is_file_stable(filepath): + print(f"检测到新文件: {filepath}") + self.add_dropped_file(filepath) + if self.drop_callback: + self.drop_callback([filepath]) + + except OSError: + continue + + known_files.update(current_files) + + except PermissionError: + continue + except Exception as e: + print(f"监控目录 {watch_dir} 时出错: {e}") + + except Exception as e: + print(f"监控循环错误: {e}") + + time.sleep(0.3) # 更频繁的检查 + + def _check_clipboard(self): + """检查剪贴板变化""" + try: + # 检查剪贴板 + result = subprocess.run(['xclip', '-selection', 'clipboard', '-o'], + capture_output=True, text=True, timeout=0.5) + if result.returncode == 0 and result.stdout.strip(): + current_clipboard = result.stdout.strip() + if current_clipboard != self.last_clipboard: + self.last_clipboard = current_clipboard + files = self._parse_clipboard_content(current_clipboard) + if files: + print(f"剪贴板检测到文件: {files}") + for file_path in files: + self.add_dropped_file(file_path) + if self.drop_callback: + self.drop_callback(files) + + except Exception: + # 尝试使用xsel + try: + result = subprocess.run(['xsel', '--clipboard', '--output'], + capture_output=True, text=True, timeout=0.5) + if result.returncode == 0 and result.stdout.strip(): + current_clipboard = result.stdout.strip() + if current_clipboard != self.last_clipboard: + self.last_clipboard = current_clipboard + files = self._parse_clipboard_content(current_clipboard) + if files: + for file_path in files: + self.add_dropped_file(file_path) + if self.drop_callback: + self.drop_callback(files) + except Exception: + pass + + def _parse_clipboard_content(self, content: str) -> List[str]: + """解析剪贴板内容""" + files = [] + for line in content.split('\n'): + line = line.strip() + if line.startswith('file://'): + file_path = line[7:] + file_path = file_path.replace('%20', ' ') + if os.path.exists(file_path): + files.append(file_path) + elif os.path.isabs(line) and os.path.exists(line): + files.append(line) + return files + + def _get_watch_directories(self) -> List[str]: + """获取监控目录""" + dirs = [ + os.path.expanduser('~/Desktop'), + os.path.expanduser('~/Downloads'), + tempfile.gettempdir() + ] + + # 添加一些常见的拖拽目标目录 + additional_dirs = [ + os.path.expanduser('~/Documents'), + os.path.expanduser('~'), + '/tmp' + ] + + for dir_path in additional_dirs: + if os.path.exists(dir_path) and os.path.isdir(dir_path): + dirs.append(dir_path) + + return [d for d in dirs if os.path.exists(d)] + + def _scan_directory(self, directory: str, known_files: set): + """扫描目录""" + try: + for filename in os.listdir(directory): + filepath = os.path.join(directory, filename) + if self._is_supported_format(filepath): + try: + known_files.add(filepath) + except OSError: + pass + except Exception: + pass + + def _is_supported_format(self, file_path: str) -> bool: + """检查文件格式是否支持""" + if not os.path.isfile(file_path): + return False + file_ext = os.path.splitext(file_path)[1].lower() + return file_ext in self.supported_formats + + def _is_file_stable(self, file_path: str, wait_time: float = 0.5) -> bool: + """检查文件是否稳定""" + try: + initial_size = os.path.getsize(file_path) + time.sleep(wait_time) + current_size = os.path.getsize(file_path) + return initial_size == current_size + except: + return False + + def is_supported(self) -> bool: + """检查是否支持""" + return True # 这个版本总是支持的 + + def get_platform_info(self) -> dict: + """获取平台信息""" + return { + 'system': 'Linux', + 'detector_type': 'SimpleDragDetector', + 'supported': True, + 'method': 'File system + clipboard monitoring' + } diff --git a/core/drag_drop/windows_detector.py b/core/drag_drop/windows_detector.py new file mode 100644 index 00000000..12945781 --- /dev/null +++ b/core/drag_drop/windows_detector.py @@ -0,0 +1,221 @@ +""" +Windows平台拖拽检测器 + +使用Windows API实现系统级拖拽检测。 +""" + +import os +import sys +import time +import threading +from typing import List, Optional +from .base_detector import BaseDragDetector + + +class WindowsDragDetector(BaseDragDetector): + """Windows平台拖拽检测器""" + + def __init__(self, supported_formats: List[str] = None): + """ + 初始化Windows拖拽检测器 + + Args: + supported_formats: 支持的文件格式列表 + """ + super().__init__(supported_formats) + self._hwnd = None + self._old_win_proc = None + self._is_com_initialized = False + self._ole_initialized = False + + def _initialize_com(self) -> bool: + """初始化COM""" + try: + import ctypes + from ctypes import wintypes + + # 初始化COM + ctypes.windll.ole32.CoInitializeEx(None, 0) # COINIT_APARTMENTTHREADED + self._is_com_initialized = True + return True + except Exception as e: + print(f"COM初始化失败: {e}") + return False + + def _initialize_ole(self) -> bool: + """初始化OLE""" + try: + import ctypes + from ctypes import wintypes + + # 初始化OLE + ctypes.windll.ole32.OleInitialize(None) + self._ole_initialized = True + return True + except Exception as e: + print(f"OLE初始化失败: {e}") + return False + + def _get_window_handle(self) -> Optional[int]: + """获取Panda3D窗口句柄""" + try: + # 这里需要获取Panda3D窗口的句柄 + # 可能需要通过Panda3D的API获取 + # 暂时返回None,需要进一步实现 + return None + except Exception as e: + print(f"获取窗口句柄失败: {e}") + return None + + def _setup_drag_drop(self) -> bool: + """设置拖拽接收""" + try: + import ctypes + from ctypes import wintypes + + hwnd = self._get_window_handle() + if not hwnd: + return False + + # 注册拖拽接收窗口 + # 这里需要实现Windows拖拽API的调用 + # 暂时返回False,需要进一步实现 + return False + except Exception as e: + print(f"设置拖拽接收失败: {e}") + return False + + def _monitor_clipboard_drag(self): + """通过剪贴板监控拖拽""" + try: + import ctypes + from ctypes import wintypes + + user32 = ctypes.windll.user32 + kernel32 = ctypes.windll.kernel32 + + # 监控剪贴板变化 + while self.is_running: + # 检查剪贴板是否包含文件 + if user32.OpenClipboard(None): + try: + # 检查是否是文件格式 + format_id = 15 # CF_HDROP + if user32.IsClipboardFormatAvailable(format_id): + # 获取拖拽文件列表 + h_drop = user32.GetClipboardData(format_id) + if h_drop: + files = self._parse_drop_files(h_drop) + for file_path in files: + self.add_dropped_file(file_path) + finally: + user32.CloseClipboard() + + time.sleep(0.1) + + except Exception as e: + print(f"剪贴板监控错误: {e}") + + def _parse_drop_files(self, h_drop) -> List[str]: + """解析拖拽文件列表""" + try: + import ctypes + from ctypes import wintypes + + kernel32 = ctypes.windll.kernel32 + shell32 = ctypes.windll.shell32 + + # 获取文件数量 + file_count = shell32.DragQueryFileW(h_drop, 0xFFFFFFFF, None, 0) + files = [] + + # 获取每个文件路径 + for i in range(file_count): + # 获取文件路径长度 + length = shell32.DragQueryFileW(h_drop, i, None, 0) + if length > 0: + # 分配缓冲区 + buffer = ctypes.create_unicode_buffer(length + 1) + # 获取文件路径 + shell32.DragQueryFileW(h_drop, i, buffer, length + 1) + files.append(buffer.value) + + return files + except Exception as e: + print(f"解析拖拽文件失败: {e}") + return [] + + def _monitor_file_drag(self): + """通过文件系统变化监控拖拽""" + # 监控用户桌面和下载目录 + import winreg + + try: + # 获取桌面路径 + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") + desktop_path = winreg.QueryValueEx(key, "Desktop")[0] + downloads_path = winreg.QueryValueEx(key, "{374DE290-123F-4565-9164-39C4925E467B}")[0] # 下载文件夹 + winreg.CloseKey(key) + except Exception: + # 降级到默认路径 + desktop_path = os.path.expanduser("~/Desktop") + downloads_path = os.path.expanduser("~/Downloads") + + watch_dirs = [desktop_path, downloads_path] + + while self.is_running: + try: + current_time = time.time() + for watch_dir in watch_dirs: + if not os.path.exists(watch_dir): + continue + + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + try: + file_time = os.path.getmtime(filepath) + # 如果文件是最近1秒内创建的 + if current_time - file_time < 1.0: + if self._is_supported_format(filepath): + self.add_dropped_file(filepath) + except OSError: + continue + + except Exception as e: + print(f"文件拖拽监控错误: {e}") + + time.sleep(0.5) + + def _monitor_loop(self): + """主监控循环""" + # 尝试初始化COM和OLE + com_ready = self._initialize_com() + ole_ready = self._initialize_ole() + + if com_ready and ole_ready: + # 尝试设置真正的拖拽接收 + if self._setup_drag_drop(): + # 使用Windows API拖拽检测 + pass + else: + # 降级到剪贴板监控 + self._monitor_clipboard_drag() + else: + # 降级到文件系统监控 + self._monitor_file_drag() + + # 清理COM和OLE + try: + if self._ole_initialized: + import ctypes + ctypes.windll.ole32.OleUninitialize() + if self._is_com_initialized: + import ctypes + ctypes.windll.ole32.CoUninitialize() + except Exception: + pass + + def is_supported(self) -> bool: + """检查Windows平台是否支持""" + return sys.platform.startswith('win') \ No newline at end of file diff --git a/core/drag_drop/x11_drag_receiver.py b/core/drag_drop/x11_drag_receiver.py new file mode 100644 index 00000000..1031e3dc --- /dev/null +++ b/core/drag_drop/x11_drag_receiver.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +""" +X11窗口拖拽事件接收器 + +使用X11的Xdnd协议实现真正的窗口拖拽功能 +""" + +import os +import sys +import time +import subprocess +import threading +from typing import List, Optional, Callable +from collections import deque + +class X11DragReceiver: + """X11窗口拖拽事件接收器""" + + def __init__(self, supported_formats: List[str]): + self.supported_formats = supported_formats + self.window_id = None + self.is_running = False + self.drop_callback = None + self.dropped_files = deque() + self.monitor_thread = None + + # X11相关 + self.x11_display = os.environ.get('DISPLAY', ':0') + self.xdnd_aware = True # 标记窗口支持拖拽 + + def set_window(self, window_id: int): + """设置要监控的窗口ID""" + self.window_id = window_id + print(f"设置拖拽监控窗口: {window_id}") + + def set_drop_callback(self, callback: Callable[[List[str]], None]): + """设置拖拽回调函数""" + self.drop_callback = callback + + def start_monitoring(self): + """开始监控拖拽事件""" + if self.is_running: + return + + self.is_running = True + self.monitor_thread = threading.Thread(target=self._monitor_drag_events) + self.monitor_thread.daemon = True + self.monitor_thread.start() + print("X11拖拽监控已启动") + + def stop_monitoring(self): + """停止监控""" + self.is_running = False + if self.monitor_thread: + self.monitor_thread.join(timeout=1) + + def get_dropped_files(self) -> List[str]: + """获取拖拽的文件列表""" + files = list(self.dropped_files) + self.dropped_files.clear() + return files + + def add_dropped_file(self, file_path: str): + """添加拖拽的文件""" + if os.path.exists(file_path): + file_ext = os.path.splitext(file_path)[1].lower() + if file_ext in self.supported_formats: + self.dropped_files.append(file_path) + print(f"检测到拖拽文件: {file_path}") + + def _monitor_drag_events(self): + """监控拖拽事件""" + if not self.window_id: + print("窗口ID未设置,无法监控拖拽事件") + return + + try: + # 首先注册窗口为拖拽目标 + self._register_drag_target() + + # 使用xev监控窗口事件 + cmd = ['xev', '-id', str(self.window_id), '-event', 'dnd'] + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True) + + print(f"开始监控窗口 {self.window_id} 的拖拽事件") + + while self.is_running: + line = process.stdout.readline() + if not line: + break + + # 解析拖拽事件 + if 'XdndEnter' in line: + self._handle_drag_enter(line) + elif 'XdndPosition' in line: + self._handle_drag_position(line) + elif 'XdndDrop' in line: + self._handle_drag_drop(line) + elif 'XdndLeave' in line: + self._handle_drag_leave(line) + + except Exception as e: + print(f"拖拽事件监控错误: {e}") + # 降级到文件监控 + self._fallback_to_file_monitor() + finally: + if 'process' in locals(): + process.terminate() + + def _register_drag_target(self): + """注册窗口为拖拽目标""" + try: + # 使用xprop设置窗口属性,标记支持拖拽 + cmd = ['xprop', '-id', str(self.window_id), '-f', '_NET_WM_WINDOW_TYPE', '32a', + '-set', '_NET_WM_WINDOW_TYPE', '_NET_WM_WINDOW_TYPE_NORMAL'] + subprocess.run(cmd, capture_output=True, timeout=2) + + # 设置XdndAware属性 + cmd = ['xprop', '-id', str(self.window_id), '-f', 'XdndAware', '32c', + '-set', 'XdndAware', '5'] + subprocess.run(cmd, capture_output=True, timeout=2) + + print(f"窗口 {self.window_id} 已注册为拖拽目标") + + except Exception as e: + print(f"注册拖拽目标失败: {e}") + + def _handle_drag_enter(self, event_line: str): + """处理拖拽进入事件""" + print("检测到拖拽进入") + + def _handle_drag_position(self, event_line: str): + """处理拖拽位置事件""" + # 可以在这里更新拖拽位置显示 + pass + + def _handle_drag_drop(self, event_line: str): + """处理拖拽释放事件""" + print("检测到拖拽释放") + + # 尝试获取拖拽的文件 + files = self._extract_dropped_files() + if files: + for file_path in files: + self.add_dropped_file(file_path) + + # 调用回调函数 + if self.drop_callback: + self.drop_callback(list(self.dropped_files)) + + def _handle_drag_leave(self, event_line: str): + """处理拖拽离开事件""" + print("拖拽离开窗口") + + def _extract_dropped_files(self) -> List[str]: + """提取拖拽的文件路径""" + files = [] + + try: + # 方法1:尝试从剪贴板获取 + result = subprocess.run(['xclip', '-selection', 'clipboard', '-o'], + capture_output=True, text=True, timeout=1) + if result.returncode == 0 and result.stdout.strip(): + files.extend(self._parse_uri_list(result.stdout.strip())) + + # 方法2:尝试从主选择获取 + result = subprocess.run(['xclip', '-selection', 'primary', '-o'], + capture_output=True, text=True, timeout=1) + if result.returncode == 0 and result.stdout.strip(): + files.extend(self._parse_uri_list(result.stdout.strip())) + + # 方法3:使用xsel作为备选 + if not files: + result = subprocess.run(['xsel', '--clipboard', '--output'], + capture_output=True, text=True, timeout=1) + if result.returncode == 0 and result.stdout.strip(): + files.extend(self._parse_uri_list(result.stdout.strip())) + + except Exception as e: + print(f"提取拖拽文件失败: {e}") + + return files + + def _parse_uri_list(self, uri_list: str) -> List[str]: + """解析URI列表,提取文件路径""" + files = [] + + for line in uri_list.split('\n'): + line = line.strip() + if not line or line.startswith('#'): + continue + + if line.startswith('file://'): + # URI格式 + file_path = line[7:] # 移除 'file://' 前缀 + file_path = file_path.replace('%20', ' ') # 解码空格 + file_path = file_path.strip() + + # 移除可能的回车符 + if file_path.endswith('\r'): + file_path = file_path[:-1] + + if os.path.exists(file_path): + files.append(file_path) + elif os.path.isabs(line): + # 绝对路径 + if os.path.exists(line): + files.append(line) + + return files + + def _fallback_to_file_monitor(self): + """降级到文件监控""" + print("降级到文件监控模式") + + # 监控桌面和下载目录 + watch_dirs = [ + os.path.expanduser('~/Desktop'), + os.path.expanduser('~/Downloads'), + '/tmp' + ] + + known_files = set() + + # 初始化已知文件 + for watch_dir in watch_dirs: + if os.path.exists(watch_dir): + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + if self._is_supported_format(filepath): + try: + known_files.add(filepath) + except OSError: + pass + + while self.is_running: + try: + current_time = time.time() + + for watch_dir in watch_dirs: + if not os.path.exists(watch_dir): + continue + + try: + for filename in os.listdir(watch_dir): + filepath = os.path.join(watch_dir, filename) + if self._is_supported_format(filepath): + try: + file_time = os.path.getmtime(filepath) + + # 检查是否是新文件(最近5秒内创建) + if filepath not in known_files and current_time - file_time < 5.0: + print(f"检测到新文件: {filepath}") + self.add_dropped_file(filepath) + + # 调用回调函数 + if self.drop_callback: + self.drop_callback([filepath]) + + known_files.add(filepath) + + except OSError: + continue + + except PermissionError: + continue + except Exception as e: + print(f"监控目录 {watch_dir} 时出错: {e}") + + except Exception as e: + print(f"文件监控错误: {e}") + + time.sleep(0.5) + + def _is_supported_format(self, file_path: str) -> bool: + """检查文件格式是否支持""" + if not os.path.isfile(file_path): + return False + + file_ext = os.path.splitext(file_path)[1].lower() + return file_ext in self.supported_formats + + def is_supported(self) -> bool: + """检查是否支持拖拽功能""" + # 检查X11环境 + if not os.environ.get('DISPLAY'): + return False + + # 检查必要工具 + required_tools = ['xev', 'xprop', 'xclip'] + for tool in required_tools: + try: + subprocess.run(['which', tool], capture_output=True, check=True) + except subprocess.CalledProcessError: + print(f"缺少必要工具: {tool}") + return False + + return True + + def get_platform_info(self) -> dict: + """获取平台信息""" + return { + 'system': 'Linux', + 'detector_type': 'X11DragReceiver', + 'supported': self.is_supported(), + 'display': self.x11_display, + 'window_id': self.window_id + } \ No newline at end of file diff --git a/core/resource_manager.py b/core/resource_manager.py new file mode 100644 index 00000000..46931884 --- /dev/null +++ b/core/resource_manager.py @@ -0,0 +1,373 @@ +""" +资源管理器模块 - 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 \ No newline at end of file diff --git a/demo.py b/demo.py index 1a2498cc..6cd644a0 100644 --- a/demo.py +++ b/demo.py @@ -31,6 +31,7 @@ from project.project_manager import ProjectManager from core.InfoPanelManager import InfoPanelManager from core.collision_manager import CollisionManager from core.CustomMouseController import CustomMouseController +from core.resource_manager import ResourceManager # 拖拽监控类 class DragDropMonitor: @@ -150,6 +151,9 @@ class MyWorld(CoreWorld): # 初始化碰撞管理器 self.collision_manager = CollisionManager(self) + # 初始化资源管理器 + self.resource_manager = ResourceManager(self) + # 初始化自定义鼠标控制器(视角移动) self.mouse_controller = CustomMouseController(self) self.mouse_controller.setUp(mouse_speed=25, move_speed=20) @@ -262,6 +266,7 @@ class MyWorld(CoreWorld): self.showConsole = True self.showScriptPanel = True self.showToolbar = True + self.showResourceManager = True # 菜单状态管理 self.show_new_project_dialog = False @@ -542,6 +547,12 @@ class MyWorld(CoreWorld): # 绘制拖拽界面 self._draw_drag_drop_interface() + + # 检查鼠标释放事件(用于处理拖拽结束) + if imgui.is_mouse_released(0) and self.is_dragging: + if not imgui.is_any_window_hovered(): + # 在3D视图中释放,处理模型导入 + self._handle_drag_drop_completion() def _draw_docked_layout(self, window_width, window_height): """绘制可停靠的布局(支持拖拽)""" @@ -549,6 +560,10 @@ class MyWorld(CoreWorld): if self.showSceneTree: self._draw_scene_tree() + # 资源管理器面板 + if self.showResourceManager: + self._draw_resource_manager() + # 属性面板 if self.showPropertyPanel: self._draw_property_panel() @@ -610,6 +625,7 @@ class MyWorld(CoreWorld): if view_menu: _, self.showToolbar = imgui.menu_item("工具栏", "", self.showToolbar, True) _, self.showSceneTree = imgui.menu_item("场景树", "", self.showSceneTree, True) + _, self.showResourceManager = imgui.menu_item("资源管理器", "", self.showResourceManager, True) _, self.showPropertyPanel = imgui.menu_item("属性面板", "", self.showPropertyPanel, True) _, self.showConsole = imgui.menu_item("控制台", "", self.showConsole, True) _, self.showScriptPanel = imgui.menu_item("脚本管理", "", self.showScriptPanel, True) @@ -739,6 +755,304 @@ class MyWorld(CoreWorld): imgui.text("(空)") imgui.tree_pop() + def _draw_resource_manager(self): + """绘制资源管理器面板""" + # 使用面板类型的窗口标志,支持docking + flags = self.style_manager.get_window_flags("panel") + + with self.style_manager.begin_styled_window("资源管理器", self.showResourceManager, flags): + self.showResourceManager = True # 确保窗口保持打开 + + # 获取资源管理器实例 + rm = self.resource_manager + + # 工具栏 + imgui.text("文件浏览器") + imgui.separator() + + # 导航按钮 + if imgui.button("◀"): + rm.navigate_back() + imgui.same_line() + if imgui.button("▶"): + rm.navigate_forward() + imgui.same_line() + if imgui.button("▲"): + rm.navigate_up() + imgui.same_line() + if imgui.button("🏠"): + rm.navigate_to(rm.project_root / "Resources") + imgui.same_line() + if imgui.button("🔄"): + rm.force_refresh() + + # 自动刷新开关 + imgui.same_line() + changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled) + if changed: + rm.set_auto_refresh(rm.auto_refresh_enabled) + + imgui.same_line() + imgui.text(" ") + imgui.same_line() + + # 路径输入框 + changed, new_path = imgui.input_text("路径", str(rm.current_path), 256) + if changed: + try: + rm.navigate_to(Path(new_path)) + except: + pass + + # 搜索框 + changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) + + imgui.separator() + + # 检查自动刷新 + if rm.refresh_if_needed(): + # 目录内容发生变化,可以在这里添加通知逻辑 + pass + + # 获取目录内容 + dirs, files = rm.get_directory_contents(rm.current_path) + + # 显示目录 + for dir_path in dirs: + if not rm.should_show_file(dir_path): + continue + + # 目录图标和名称 + icon = rm.get_file_icon(dir_path.name, is_folder=True) + node_open = False + + # 检查是否被选中 + is_selected = dir_path in rm.selected_files + + # 使用TreeNode来显示目录 + if is_selected: + imgui.push_style_color(imgui.Col_.header, imgui.get_style().colors[imgui.Col_.header_hovered]) + + node_open = imgui.tree_node(f"{icon} {dir_path.name}") + + if is_selected: + imgui.pop_style_color() + + # 处理选择 + if imgui.is_item_clicked(): + if imgui.get_io().key_ctrl: + # 多选模式 + if is_selected: + rm.selected_files.discard(dir_path) + else: + rm.selected_files.add(dir_path) + else: + # 单选模式 + rm.selected_files.clear() + rm.selected_files.add(dir_path) + rm.focused_file = dir_path + + # 双击导航 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + rm.navigate_to(dir_path) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = dir_path + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + # 如果节点展开,显示子内容 + if node_open: + # 获取子目录内容 + subdirs, subfiles = rm.get_directory_contents(dir_path) + + # 显示子目录 + for subdir in subdirs: + if not rm.should_show_file(subdir): + continue + + subicon = rm.get_file_icon(subdir.name, is_folder=True) + sub_is_selected = subdir in rm.selected_files + + if sub_is_selected: + imgui.push_style_color(imgui.Col_.header, imgui.get_style().colors[imgui.Col_.header_hovered]) + + sub_node_open = imgui.tree_node(f" {subicon} {subdir.name}") + + if sub_is_selected: + imgui.pop_style_color() + + # 处理子目录的选择 + if imgui.is_item_clicked(): + if imgui.get_io().key_ctrl: + if sub_is_selected: + rm.selected_files.discard(subdir) + else: + rm.selected_files.add(subdir) + else: + rm.selected_files.clear() + rm.selected_files.add(subdir) + rm.focused_file = subdir + + # 双击子目录导航 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + rm.navigate_to(subdir) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = subdir + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + if sub_node_open: + imgui.tree_pop() + + # 显示子文件 + for subfile in subfiles: + if not rm.should_show_file(subfile): + continue + + subicon = rm.get_file_icon(subfile.name) + sub_is_selected = subfile in rm.selected_files + + selected = imgui.selectable(f" {subicon} {subfile.name}", sub_is_selected) + + # 处理子文件的选择 + if selected: + if imgui.get_io().key_ctrl: + if sub_is_selected: + rm.selected_files.discard(subfile) + else: + rm.selected_files.add(subfile) + else: + rm.selected_files.clear() + rm.selected_files.add(subfile) + rm.focused_file = subfile + + # 双击子文件操作 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + if subfile.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + self.scene_manager.importModel(str(subfile)) + self.add_info_message(f"正在导入模型: {subfile.name}") + else: + rm.open_file(subfile) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = subfile + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + imgui.tree_pop() + + # 显示文件 + for file_path in files: + if not rm.should_show_file(file_path): + continue + + # 文件图标和名称 + icon = rm.get_file_icon(file_path.name) + file_size = rm.get_file_size_string(file_path) + + # 检查是否被选中 + is_selected = file_path in rm.selected_files + + # 使用Selectable来显示文件 + selected = imgui.selectable(f"{icon} {file_path.name}", is_selected) + + # 显示文件大小 + if file_size: + imgui.same_line() + imgui.text_disabled(file_size) + + # 处理选择 + if selected: + if imgui.get_io().key_ctrl: + # 多选模式 + if is_selected: + rm.selected_files.discard(file_path) + else: + rm.selected_files.add(file_path) + else: + # 单选模式 + rm.selected_files.clear() + rm.selected_files.add(file_path) + rm.focused_file = file_path + + # 处理拖拽开始 + if imgui.is_item_active() and imgui.is_item_hovered(): + if imgui.is_mouse_dragging(0): + # 开始拖拽 + drag_files = list(rm.selected_files) if rm.selected_files else [file_path] + rm.start_drag(drag_files) + self.is_dragging = True + self.show_drag_overlay = True + + # 双击打开文件 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + # 检查是否是支持的3D模型格式 + if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + # 导入3D模型 + self.add_info_message(f"正在导入模型: {file_path.name}") + self.scene_manager.importModel(str(file_path)) + else: + # 使用系统默认程序打开 + rm.open_file(file_path) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = file_path + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + # 右键菜单 + if rm.show_context_menu and rm.context_menu_file: + imgui.set_next_window_position(rm.context_menu_position[0], rm.context_menu_position[1]) + with imgui_ctx.begin_popup("context_menu", imgui.WindowFlags_.no_title_bar | + imgui.WindowFlags_.no_resize | imgui.WindowFlags_.always_auto_resize) as popup: + if popup: + if rm.context_menu_file.is_dir(): + if imgui.menu_item("打开"): + rm.navigate_to(rm.context_menu_file) + imgui.separator() + if imgui.menu_item("重命名"): + print(f"重命名文件夹: {rm.context_menu_file.name}") + if imgui.menu_item("删除"): + print(f"删除文件夹: {rm.context_menu_file.name}") + else: + if imgui.menu_item("打开"): + rm.open_file(rm.context_menu_file) + imgui.separator() + if imgui.menu_item("导入到场景"): + if rm.context_menu_file.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + self.add_info_message(f"正在导入模型: {rm.context_menu_file.name}") + self.scene_manager.importModel(str(rm.context_menu_file)) + if imgui.menu_item("重命名"): + print(f"重命名文件: {rm.context_menu_file.name}") + if imgui.menu_item("删除"): + print(f"删除文件: {rm.context_menu_file.name}") + + imgui.separator() + if imgui.menu_item("复制路径"): + imgui.set_clipboard_text(str(rm.context_menu_file)) + self.add_info_message("路径已复制到剪贴板") + if imgui.menu_item("在文件管理器中显示"): + import platform + import subprocess + if platform.system() == "Windows": + subprocess.run(["explorer", "/select,", str(rm.context_menu_file)]) + elif platform.system() == "Darwin": + subprocess.run(["open", "-R", str(rm.context_menu_file)]) + else: + subprocess.run(["xdg-open", str(rm.context_menu_file.parent)]) + + # 如果点击其他地方,关闭菜单 + if imgui.is_mouse_clicked(0) or imgui.is_mouse_clicked(1): + if not imgui.is_window_hovered(): + rm.show_context_menu = False + rm.context_menu_file = None + def _draw_property_panel(self): """绘制属性面板""" # 使用面板类型的窗口标志,支持docking @@ -1984,6 +2298,12 @@ class MyWorld(CoreWorld): def _draw_drag_drop_interface(self): """绘制拖拽界面""" + # 检查资源管理器的拖拽状态 + if self.resource_manager.is_dragging(): + self.is_dragging = True + self.dragged_files = self.resource_manager.get_dragged_files() + self.show_drag_overlay = True + # 绘制拖拽覆盖层 if self.show_drag_overlay: self._draw_drag_overlay() @@ -1992,6 +2312,38 @@ class MyWorld(CoreWorld): if self.is_dragging and self.dragged_files: # 显示拖拽状态 self._draw_drag_status() + + # 检查是否释放鼠标(结束拖拽) + if imgui.is_mouse_released(0): + self._handle_drag_drop_completion() + + def _handle_drag_drop_completion(self): + """处理拖拽完成""" + # 检查是否在3D视图中释放 + mouse_pos = imgui.get_mouse_pos() + viewport = imgui.get_main_viewport() + + # 简单检查:如果不在任何ImGui窗口上,则认为是在3D视图中 + if not imgui.is_any_window_hovered(): + # 导入支持的3D模型文件 + imported_count = 0 + for file_path in self.dragged_files: + if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + try: + self.scene_manager.importModel(str(file_path)) + self.add_success_message(f"成功导入模型: {file_path.name}") + imported_count += 1 + except Exception as e: + self.add_error_message(f"导入模型失败 {file_path.name}: {e}") + + if imported_count > 0: + self.add_success_message(f"共导入 {imported_count} 个模型") + + # 清除拖拽状态 + self.is_dragging = False + self.dragged_files.clear() + self.show_drag_overlay = False + self.resource_manager.clear_drag() def _draw_drag_overlay(self): """绘制拖拽覆盖层""" diff --git a/imgui.ini b/imgui.ini index 0de698f2..0b653654 100644 --- a/imgui.ini +++ b/imgui.ini @@ -1,5 +1,5 @@ [Window][Debug##Default] -Pos=60,60 +Pos=28,731 Size=400,400 Collapsed=0 @@ -31,7 +31,7 @@ DockId=0x00000007,0 [Window][场景树] Pos=0,20 -Size=285,883 +Size=285,753 Collapsed=0 DockId=0x00000001,0 @@ -42,10 +42,10 @@ Collapsed=0 DockId=0x00000005,0 [Window][控制台] -Pos=0,905 -Size=1524,111 +Pos=880,775 +Size=644,241 Collapsed=0 -DockId=0x0000000A,0 +DockId=0x0000000C,0 [Window][脚本管理] Pos=1526,520 @@ -98,15 +98,23 @@ Pos=625,258 Size=600,500 Collapsed=0 +[Window][资源管理器] +Pos=0,775 +Size=878,241 +Collapsed=0 +DockId=0x0000000B,0 + [Docking][Data] DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1524,996 Split=Y - DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,883 Split=X + DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,753 Split=X DockNode ID=0x00000001 Parent=0x00000009 SizeRef=285,730 HiddenTabBar=1 Selected=0xE0015051 DockNode ID=0x00000002 Parent=0x00000009 SizeRef=1237,730 Split=Y DockNode ID=0x00000007 Parent=0x00000002 SizeRef=1380,32 HiddenTabBar=1 Selected=0x43A39006 - DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,849 CentralNode=1 Selected=0x5E5F7166 - DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,111 HiddenTabBar=1 Selected=0x5428E753 + DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,719 CentralNode=1 Selected=0x5E5F7166 + DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,241 Split=X Selected=0x5428E753 + DockNode ID=0x0000000B Parent=0x0000000A SizeRef=878,111 HiddenTabBar=1 Selected=0x3A2E05C3 + DockNode ID=0x0000000C Parent=0x0000000A SizeRef=644,111 HiddenTabBar=1 Selected=0x5428E753 DockNode ID=0x00000004 Parent=0x08BD597D SizeRef=324,996 Split=Y Selected=0x5DB6FF37 DockNode ID=0x00000005 Parent=0x00000004 SizeRef=304,498 HiddenTabBar=1 Selected=0x5DB6FF37 DockNode ID=0x00000006 Parent=0x00000004 SizeRef=304,496 HiddenTabBar=1 Selected=0x3188AB8D