311 lines
12 KiB
Python
311 lines
12 KiB
Python
#!/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
|
||
} |