EG/core/drag_drop/x11_drag_receiver.py
2026-01-21 09:59:13 +08:00

311 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
}