功能:接入Linux原生光标并完善设备接入工具

This commit is contained in:
songyiming committed 2026-11-05 19:18:54 +08:00
1 parent a14f028402
commit a47968496e
92 files changed
+3090 -22

No files matched your search

+243 -2
View File
@@ -3,12 +3,25 @@
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QEvent>
#include <QEventLoop>
#include <QFile>
#include <QKeyEvent>
#include <QMenu>
#include <QShortcut>
#include <QScreen>
#include <QShowEvent>
#include <QStackedWidget>
#include <QStatusBar>
#include <QTimer>
#include <QWidget>
#include <QWindow>
#ifdef Q_OS_UNIX
#include <X11/Xlib.h>
#undef KeyPress
#undef KeyRelease
#endif
#include "desktop_list_page.h"
#include "desktop_settings_dialog.h"
@@ -46,13 +59,42 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
}
MainWindow::~MainWindow() {
qApp->removeEventFilter(this);
setKioskInputGuard(false);
#ifdef Q_OS_UNIX
if (grabDisplay_) {
XCloseDisplay(static_cast<Display*>(grabDisplay_));
grabDisplay_ = nullptr;
}
#endif
stopSessions();
delete returnCover_;
returnCover_ = nullptr;
}
void MainWindow::setupUi() {
setWindowTitle(QStringLiteral("VDI 云终端"));
// The login/list pages are the security boundary of the terminal. Keep a
// borderless, top-level surface over the desktop panels while the guest
// stream is not active. X11BypassWindowManagerHint also prevents MATE's
// panel from being raised above the shell after a global shortcut.
Qt::WindowFlags kioskFlags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint;
#ifdef Q_OS_UNIX
kioskFlags |= Qt::X11BypassWindowManagerHint;
#endif
setWindowFlags(kioskFlags);
setWindowState(Qt::WindowFullScreen);
setStyleSheet(QStringLiteral("QMainWindow { background: #0f1420; }"));
qApp->installEventFilter(this);
Qt::WindowFlags coverFlags = Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool;
#ifdef Q_OS_UNIX
coverFlags |= Qt::X11BypassWindowManagerHint;
#endif
returnCover_ = new QWidget(nullptr, coverFlags);
returnCover_->setAttribute(Qt::WA_ShowWithoutActivating);
returnCover_->setStyleSheet(QStringLiteral("QWidget { background: #0f1420; }"));
returnCover_->hide();
stack_ = new QStackedWidget(this);
stack_->setStyleSheet(QStringLiteral("background: #0f1420;"));
@@ -120,12 +162,18 @@ void MainWindow::setupHotkey() {
connect(quitShortcut, &QShortcut::activated, this, [this]() {
if (fsm_.isIn(State::kConnecting) || fsm_.isIn(State::kSession) ||
fsm_.isIn(State::kReconnect)) {
stopSessions();
// Put the opaque kiosk surface back on top before asking
// Moonlight to terminate. This prevents one compositor frame of
// the box desktop from showing while the stream window closes.
showReturnCover();
fsm_.transitionTo(State::kDesktopList, QStringLiteral("已退出串流"));
presentKioskSurface();
stopSessions();
if (desktopListPage_) {
desktopListPage_->setBusy(false);
desktopListPage_->setStatus(QStringLiteral("已退出串流"));
}
releaseReturnCoverWhenReady();
}
});
}
@@ -148,6 +196,21 @@ void MainWindow::onStateChanged(State from, State to, const QString& detail) {
const QString stateName = QString::fromLatin1(stateToString(to));
updateStatusBar(detail.isEmpty() ? stateName : QStringLiteral("%1 %2").arg(stateName, detail));
// Only the Moonlight window may receive system shortcuts. Every other
// state keeps the kiosk keyboard grab, including the connecting overlay.
const bool streamSurface = to == State::kSession || to == State::kReconnect;
if (streamSurface) {
if (isVisible()) {
hide();
}
setKioskInputGuard(false);
} else {
setKioskInputGuard(true);
if (to != State::kBoot) {
presentKioskSurface();
}
}
switch (to) {
case State::kConfig:
showLogin();
@@ -177,6 +240,182 @@ void MainWindow::onStateChanged(State from, State to, const QString& detail) {
}
}
void MainWindow::showEvent(QShowEvent* event) {
QMainWindow::showEvent(event);
applyKioskSurface();
if (kioskInputGuard_) {
retryKeyboardGrab();
}
}
void MainWindow::applyKioskSurface() {
if (!isVisible()) {
return;
}
raise();
activateWindow();
if (QScreen* screen = windowHandle() ? windowHandle()->screen() : QApplication::primaryScreen()) {
// An override-redirect window does not receive the WM fullscreen
// negotiation; explicitly cover the complete output geometry.
setGeometry(screen->geometry());
}
}
void MainWindow::presentKioskSurface() {
if (!isVisible()) {
showFullScreen();
}
// Let Qt map and paint the opaque surface before changing the X11 stack.
// The direct X11 calls are needed because Moonlight uses an override/full-
// screen window and a WM-only raise can otherwise be deferred one frame.
applyKioskSurface();
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
#ifdef Q_OS_UNIX
if (!grabDisplay_) {
grabDisplay_ = XOpenDisplay(nullptr);
}
if (grabDisplay_) {
auto* display = static_cast<Display*>(grabDisplay_);
const Window window = static_cast<Window>(winId());
XMapRaised(display, window);
XRaiseWindow(display, window);
XSetInputFocus(display, window, RevertToParent, CurrentTime);
XSync(display, False);
}
#endif
applyKioskSurface();
}
void MainWindow::showReturnCover() {
if (!returnCover_) {
return;
}
QScreen* screen = QApplication::primaryScreen();
if (screen) {
returnCover_->setGeometry(screen->geometry());
}
returnCover_->show();
returnCover_->raise();
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
#ifdef Q_OS_UNIX
if (!grabDisplay_) {
grabDisplay_ = XOpenDisplay(nullptr);
}
if (grabDisplay_) {
auto* display = static_cast<Display*>(grabDisplay_);
const Window window = static_cast<Window>(returnCover_->winId());
XMapRaised(display, window);
XRaiseWindow(display, window);
XSync(display, False);
}
#endif
}
void MainWindow::hideReturnCover() {
if (!returnCover_) {
return;
}
returnCover_->hide();
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
void MainWindow::releaseReturnCoverWhenReady() {
if (!returnCover_ || !returnCover_->isVisible()) {
return;
}
bool ready = isVisible();
#ifdef Q_OS_UNIX
if (ready && grabDisplay_) {
XWindowAttributes attributes{};
ready = XGetWindowAttributes(static_cast<Display*>(grabDisplay_),
static_cast<Window>(winId()), &attributes) != 0 &&
attributes.map_state == IsViewable;
}
#endif
if (ready) {
presentKioskSurface();
hideReturnCover();
return;
}
// The X server may need a few event-loop turns to map the Qt surface
// after Moonlight relinquishes its fullscreen window. Keep the opaque
// cover in place until that mapping is observable; never expose the root
// desktop during this interval.
QTimer::singleShot(20, this, [this]() { releaseReturnCoverWhenReady(); });
}
void MainWindow::setKioskInputGuard(bool enabled) {
kioskInputGuard_ = enabled;
if (!enabled) {
#ifdef Q_OS_UNIX
if (keyboardGrabActive_ && grabDisplay_) {
auto* display = static_cast<Display*>(grabDisplay_);
XUngrabKeyboard(display, CurrentTime);
XFlush(display);
}
#endif
keyboardGrabActive_ = false;
return;
}
if (isVisible()) {
retryKeyboardGrab();
}
}
void MainWindow::retryKeyboardGrab() {
if (!kioskInputGuard_ || !isVisible() || keyboardGrabActive_) {
return;
}
#ifdef Q_OS_UNIX
if (!grabDisplay_) {
grabDisplay_ = XOpenDisplay(nullptr);
}
if (grabDisplay_) {
auto* display = static_cast<Display*>(grabDisplay_);
const int result = XGrabKeyboard(display, static_cast<Window>(winId()), False,
GrabModeAsync, GrabModeAsync, CurrentTime);
if (result == GrabSuccess) {
XSetInputFocus(display, static_cast<Window>(winId()), RevertToParent, CurrentTime);
}
XFlush(display);
keyboardGrabActive_ = result == GrabSuccess;
}
#else
keyboardGrabActive_ = true;
#endif
if (!keyboardGrabActive_ && !keyboardGrabRetryPending_) {
keyboardGrabRetryPending_ = true;
QTimer::singleShot(200, this, [this]() {
keyboardGrabRetryPending_ = false;
retryKeyboardGrab();
});
}
}
bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
Q_UNUSED(watched);
if (kioskInputGuard_ &&
(event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease ||
event->type() == QEvent::ShortcutOverride)) {
auto* keyEvent = static_cast<QKeyEvent*>(event);
const Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
const int key = keyEvent->key();
const bool metaShortcut = modifiers.testFlag(Qt::MetaModifier) || key == Qt::Key_Meta;
const bool altWindowShortcut = modifiers.testFlag(Qt::AltModifier) &&
(key == Qt::Key_Tab || key == Qt::Key_F4);
const bool vtShortcut = modifiers.testFlag(Qt::ControlModifier) &&
modifiers.testFlag(Qt::AltModifier) &&
(key == Qt::Key_Delete || (key >= Qt::Key_F1 && key <= Qt::Key_F12));
const bool screenshotKey = key == Qt::Key_Print;
if (metaShortcut || altWindowShortcut || vtShortcut || screenshotKey) {
return true;
}
}
return QMainWindow::eventFilter(watched, event);
}
void MainWindow::updateStatusBar(const QString& text) {
statusBar_->showMessage(text);
}
@@ -443,10 +682,12 @@ void MainWindow::onMenuAction(int action) {
}
break;
case 2:
stopSessions();
showReturnCover();
if (fsm_.current() != State::kConfig) {
fsm_.transitionTo(State::kDesktopList, QStringLiteral("已断开"));
}
stopSessions();
releaseReturnCoverWhenReady();
break;
case 3:
onQuitRequested();