feat(ui): cross-platform screenshot hotkey and portal-based capture

Windows keeps the legacy path; Linux X11 uses a native XGrabKey hotkey (ctypes libX11) with native capture; Wayland captures through xdg-desktop-portal and degrades gracefully with explicit capability-unavailable logs when the portal/protocol is missing.
This commit is contained in:
2026-09-17 16:40:05 +08:00
parent 95024785ae
commit 0a62877cde
6 changed files with 1014 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""P1-04:桌面会话探测 + 截图/全局热键能力路由(纯 stdlib,可在任何平台导入)。
契约(REPAIR_BACKLOG P1-04):
- Windows 保持现有行为(Win32 RegisterHotKey + Qt grabWindow 覆盖层)。
- Linux X11:原生全局快捷键(XGrabKey,见 x11_hotkey.py+ 原生屏幕捕获(Qt grabWindowX11 可用)。
- Linux Wayland:截图走 xdg-desktop-portal(用户授权,不绕过 compositor,见 portal_capture.py);
全局热键依赖 compositor 桌面协议(ext-global-shortcut 等),本版本无免依赖实现 →
明确告知不可用,仅保留应用内 Alt+S 快捷键,主程序其余功能不受影响。
- offscreen/无显示(unknown):能力不可用要有明确日志,主程序仍可聊天。
"""
import os
import sys
def session_kind() -> str:
"""返回 "win32" / "x11" / "wayland" / "unknown"
判定顺序(Linux,综合 Qt 平台名与 XDG_SESSION_TYPE,见 PLATFORM_PLAN):
1. QT_QPA_PLATFORM 以 offscreen 开头 → unknown(自动化离屏,无桌面能力)
2. QT_QPA_PLATFORM 以 wayland 开头,或 WAYLAND_DISPLAY 已设置,
或 XDG_SESSION_TYPE=wayland → wayland
3. DISPLAY 已设置,或 XDG_SESSION_TYPE=x11 → x11
4. 其余(headless/无显示)→ unknown
"""
if sys.platform == "win32":
return "win32"
if sys.platform != "linux":
# 其他 POSIX(macOS 等)不在本任务支持矩阵内
return "unknown"
qt_plat = os.environ.get("QT_QPA_PLATFORM", "").strip()
if qt_plat.startswith("offscreen"):
return "unknown"
xdg_type = os.environ.get("XDG_SESSION_TYPE", "").strip().lower()
if (qt_plat.startswith("wayland")
or os.environ.get("WAYLAND_DISPLAY", "").strip()
or xdg_type == "wayland"):
return "wayland"
if os.environ.get("DISPLAY", "").strip() or xdg_type == "x11":
return "x11"
return "unknown"
def hotkey_plan(kind: str):
"""全局热键能力路由 → (thread_factory | None, message)。
thread_factory() 返回 QThread(带 triggered 信号);None 表示无全局热键能力
(调用方应保留应用内 QShortcut 兜底)。message 需要打印以明确当前能力。
"""
if kind == "win32":
from ui.views.system_tools.global_hotkey import GlobalHotkeyThread
return GlobalHotkeyThread, "[GlobalHotkey] Windows:系统级全局热键 Alt+SRegisterHotKey"
if kind == "x11":
from ui.views.system_tools.x11_hotkey import X11HotkeyThread
return X11HotkeyThread, "[GlobalHotkey] X11:原生全局热键 Alt+SXGrabKey"
if kind == "wayland":
return None, ("[GlobalHotkey] Wayland:全局热键需要 compositor 桌面协议"
"ext-global-shortcut 等),本版本未启用 → 仅提供应用内 Alt+S 快捷键"
"(窗口获焦时生效)与截图按钮;其余功能不受影响")
return None, ("[GlobalHotkey] 当前环境无显示服务(offscreen/无 DISPLAY)→ 全局热键不可用;"
"应用内 Alt+S 快捷键同样不可用,可用截图按钮以外的全部功能")
def capture_plan(kind: str):
"""截图能力路由 → (mode, message)。
mode: "overlay"Qt grabWindow 覆盖层,win32/x11
"portal"xdg-desktop-portal 交互截图,wayland
"unavailable"(unknown:明确告知,主程序其余功能不受影响)
"""
if kind in ("win32", "x11"):
return "overlay", None
if kind == "wayland":
return "portal", None
return "unavailable", ("[Screenshot] 当前环境无法获取屏幕画面(offscreen/无显示)→ "
"截图功能不可用;聊天与其他功能不受影响")