功能:接入Linux原生光标并完善设备接入工具
This commit is contained in:
1 parent
a14f028402
commit
a47968496e
92 files changed
+3090
-22
No files matched your search
@@ -40,6 +40,11 @@ target_link_libraries(kiosk-shell PRIVATE
|
||||
Qt5::Widgets
|
||||
)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
find_package(X11 REQUIRED)
|
||||
target_link_libraries(kiosk-shell PRIVATE X11::X11)
|
||||
endif()
|
||||
|
||||
# 看门狗 Monitor:独立进程,监控主进程假死/崩溃
|
||||
add_executable(vdi-kiosk-monitor
|
||||
src/monitor_main.cpp
|
||||
|
||||
@@ -183,6 +183,11 @@ build_cmd() {
|
||||
CMD="$CMD --video-codec $CODEC"
|
||||
CMD="$CMD --video-decoder $DECODER"
|
||||
CMD="$CMD --keep-awake"
|
||||
# The kiosk must keep system shortcuts inside the streamed desktop. Without
|
||||
# this, combinations such as Win+Shift+S are handled by the box OS and can
|
||||
# expose its UI over the cloud desktop. Apply this to Windows, Ubuntu and
|
||||
# Kylin sessions alike.
|
||||
CMD="$CMD --capture-system-keys always"
|
||||
[ "$ABSOLUTE_MOUSE" = "1" ] && CMD="$CMD --absolute-mouse"
|
||||
[ "$AUDIO" = "1" ] && CMD="$CMD --audio-on-host" || CMD="$CMD --no-audio-on-host"
|
||||
# 按profile选择同步策略;默认保持当前稳定档。
|
||||
|
||||
@@ -35,4 +35,7 @@ if [ "${1:-}" = stream ] && [ -z "${COLOR_RANGE_OVERRIDE+x}" ]; then
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [ -f "$HOME/vdi-terminal/bin/vdi-native-cursor" ]; then
|
||||
exec /usr/bin/python3 "$HOME/vdi-terminal/bin/vdi-native-cursor" "$client" "$@"
|
||||
fi
|
||||
exec "$client" "$@"
|
||||
@@ -16,7 +16,7 @@ int main(int argc, char** argv) {
|
||||
app.setOrganizationName(QStringLiteral("vdi"));
|
||||
|
||||
auto* window = new vdi::MainWindow();
|
||||
window->show();
|
||||
window->showFullScreen();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
class QStackedWidget;
|
||||
class QStatusBar;
|
||||
class QTimer;
|
||||
class QEvent;
|
||||
class QShowEvent;
|
||||
class QWidget;
|
||||
|
||||
namespace vdi {
|
||||
|
||||
@@ -33,6 +36,10 @@ public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void onLoginRequested(const QString& username, const QString& password);
|
||||
void onDesktopSelected(const QString& desktopId);
|
||||
@@ -67,6 +74,13 @@ private:
|
||||
void showLogin();
|
||||
void showDesktopList();
|
||||
void updateStatusBar(const QString& text);
|
||||
void setKioskInputGuard(bool enabled);
|
||||
void applyKioskSurface();
|
||||
void presentKioskSurface();
|
||||
void showReturnCover();
|
||||
void hideReturnCover();
|
||||
void releaseReturnCoverWhenReady();
|
||||
void retryKeyboardGrab();
|
||||
|
||||
StateMachine fsm_;
|
||||
DesktopService desktopService_;
|
||||
@@ -88,6 +102,13 @@ private:
|
||||
quint64 connectionAttempt_ = 0;
|
||||
ConnectionPhase connectionPhase_ = ConnectionPhase::kIdle;
|
||||
TerminalConfig config_;
|
||||
QWidget* returnCover_ = nullptr;
|
||||
bool kioskInputGuard_ = true;
|
||||
bool keyboardGrabActive_ = false;
|
||||
bool keyboardGrabRetryPending_ = false;
|
||||
#ifdef Q_OS_UNIX
|
||||
void* grabDisplay_ = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace vdi
|
||||
|
||||
@@ -157,6 +157,11 @@ QStringList MoonlightSession::buildStreamArguments(const ConnectionTicket& ticke
|
||||
<< QStringLiteral("--video-codec") << options.videoCodec
|
||||
<< QStringLiteral("--video-decoder") << options.decoder
|
||||
<< QStringLiteral("--absolute-mouse")
|
||||
// The terminal is a kiosk: system shortcuts must be delivered to the
|
||||
// streamed desktop instead of being handled by the box OS. This is
|
||||
// required for Win+Shift+S, Alt+Tab, Super shortcuts, etc. on every
|
||||
// supported desktop guest (Windows, Ubuntu and Kylin).
|
||||
<< QStringLiteral("--capture-system-keys") << QStringLiteral("always")
|
||||
<< QStringLiteral("--keep-awake")
|
||||
<< (options.vsync ? QStringLiteral("--vsync") : QStringLiteral("--no-vsync"))
|
||||
<< (options.framePacing ? QStringLiteral("--frame-pacing")
|
||||
|
||||
@@ -134,6 +134,9 @@ void MoonlightSessionTest::buildsConsistentStreamArguments() {
|
||||
QVERIFY(args.contains(QStringLiteral("--no-frame-pacing")));
|
||||
QVERIFY(args.contains(QStringLiteral("--performance-overlay")));
|
||||
QVERIFY(args.contains(QStringLiteral("--absolute-mouse")));
|
||||
const int captureOption = args.indexOf(QStringLiteral("--capture-system-keys"));
|
||||
QVERIFY(captureOption >= 0);
|
||||
QCOMPARE(args.value(captureOption + 1), QStringLiteral("always"));
|
||||
}
|
||||
|
||||
void MoonlightSessionTest::selectsProfileWithoutLosingStats() {
|
||||
|
||||
Reference in new issue
Block a user