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

395 lines
16 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.

"""
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