fix: restore remote keyboard control in Slint client, refs #91

This commit is contained in:
dijunkun
2026-07-28 13:53:08 +08:00
parent e3d15f8f9a
commit 7c3b13409c
11 changed files with 139 additions and 24 deletions
@@ -1,5 +1,6 @@
#include "keyboard_capturer.h"
#include <cstdint>
#include <unordered_map>
#include "keyboard_converter.h"
@@ -10,6 +11,7 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') {
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) {
(void)proxy;
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
kCrossDeskInjectedKeyboardEvent) {
return event;
}
if (!g_on_key_action) {
return event;
}
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
}
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1;
}
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event);
}
@@ -49,6 +49,9 @@ static bool PreferSideSpecificVkInjection(int key_code) {
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_on_key_action) {
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if ((kbData->flags & LLKHF_INJECTED) != 0) {
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
const int key_code = NormalizeModifierVkCode(kbData);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
+1 -1
View File
@@ -65,7 +65,7 @@ struct InteractionState {
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_sdl_events_ = false;
bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false;
bool audio_capture_ = false;
+73 -11
View File
@@ -30,6 +30,8 @@
#include "localization.h"
#include "platform.h"
#if _WIN32
#include <windows.h>
#include "platform/tray/win_tray.h"
#elif defined(__APPLE__)
#include "platform/tray/mac_tray.h"
@@ -1726,6 +1728,8 @@ void GuiApplication::BindStreamCallbacks() {
bool control, bool alt, bool shift, bool meta) {
SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
});
stream->on_keyboard_focus_changed(
[this](bool focused) { SetStreamKeyboardFocus(focused); });
}
void GuiApplication::BindServerCallbacks() {
@@ -1831,6 +1835,11 @@ void GuiApplication::Tick() {
HandlePendingPresenceProbe();
HandleConnectionTimeouts();
HandleWindowsServiceIntegration();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#else
SyncStreamKeyboardFocus();
#endif
devices_.UpdateInteractions();
UpdateLocalization();
@@ -1839,9 +1848,6 @@ void GuiApplication::Tick() {
SyncPlatformDialogs();
SyncStreamWindow();
SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#endif
}
void GuiApplication::UpdateLocalization() {
@@ -2228,6 +2234,7 @@ void GuiApplication::SyncXWaylandWindowActivation() {
Display* display = ui_->x11_focus_display;
const ::Window active_window = X11GetActiveWindow(display);
if (!active_window) {
SetStreamKeyboardFocus(false);
return;
}
@@ -2237,19 +2244,49 @@ void GuiApplication::SyncXWaylandWindowActivation() {
if (ui_->stream) {
(*ui_->stream)->set_window_active(false);
}
SetStreamKeyboardFocus(false);
return;
}
ui_->main->set_window_active(
X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) {
(*ui_->stream)
->set_window_active(
X11WindowMatches(display, active_window, (*ui_->stream)->window()));
const bool stream_active =
X11WindowMatches(display, active_window, (*ui_->stream)->window());
(*ui_->stream)->set_window_active(stream_active);
SetStreamKeyboardFocus(stream_active);
} else {
SetStreamKeyboardFocus(false);
}
}
#endif
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
if (focus_on_stream_window_ == focused) {
return;
}
focus_on_stream_window_ = focused;
if (!focused) {
keyboard_.ForceReleasePressedKeys();
}
}
void GuiApplication::SyncStreamKeyboardFocus() {
if (!ui_ || !ui_->stream || !(*ui_->stream)->window().is_visible()) {
SetStreamKeyboardFocus(false);
return;
}
#if _WIN32
const HWND stream_hwnd = (*ui_->stream)->window().win32_hwnd();
SetStreamKeyboardFocus(stream_hwnd != nullptr &&
GetForegroundWindow() == stream_hwnd);
#elif defined(__APPLE__)
SetStreamKeyboardFocus(IsStreamWindowActive());
#endif
}
void GuiApplication::SyncStreamWindow() {
bool has_sessions = false;
{
@@ -2306,6 +2343,7 @@ void GuiApplication::SyncStreamWindow() {
}
}
if (!has_sessions) {
SetStreamKeyboardFocus(false);
#if defined(__APPLE__)
SetStreamWindowFullscreen(false);
#endif
@@ -2787,14 +2825,25 @@ void GuiApplication::SelectStreamTab(int index) {
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
return;
}
focused_remote_id_ = ui_->tab_ids[index];
controlled_remote_id_ = focused_remote_id_;
const std::string selected_remote_id = ui_->tab_ids[index];
if (!controlled_remote_id_.empty() &&
controlled_remote_id_ != selected_remote_id) {
keyboard_.ForceReleasePressedKeys();
}
focused_remote_id_ = selected_remote_id;
controlled_remote_id_ = selected_remote_id;
std::shared_lock lock(remote_sessions_mutex_);
for (auto& [id, props] : remote_sessions_) {
if (props) {
props->tab_selected_ = id == focused_remote_id_;
}
}
const auto selected = remote_sessions_.find(selected_remote_id);
start_keyboard_capturer_ =
selected != remote_sessions_.end() && selected->second &&
selected->second->control_mouse_ &&
selected->second->connection_status_.load() ==
ConnectionStatus::Connected;
}
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
@@ -2982,10 +3031,23 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
(void)alt;
(void)shift;
(void)meta;
if (auto props = SelectedSession()) {
controlled_remote_id_ = props->remote_id_;
focused_remote_id_ = props->remote_id_;
auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ ||
props->connection_status_.load() != ConnectionStatus::Connected) {
return;
}
controlled_remote_id_ = props->remote_id_;
focused_remote_id_ = props->remote_id_;
// Native hooks see the same physical key before Slint does. When a native
// hook is active, forwarding the FocusScope callback as well would duplicate
// the event on platforms whose hook does not consume the local key.
if (keyboard_capturer_is_started_ &&
!keyboard_capturer_uses_window_events_) {
return;
}
const int virtual_key = SlintKeyToWindowsVk(text);
if (virtual_key >= 0) {
keyboard_.SendKeyCommand(virtual_key, pressed);
+2
View File
@@ -35,6 +35,8 @@ private:
void SyncConnectionDialog();
void SyncPlatformDialogs();
void SyncStreamWindow();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
+2 -1
View File
@@ -200,7 +200,8 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ &&
if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event);
@@ -228,27 +228,27 @@ int SessionDeviceManager::StopMouseController() {
}
int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false;
owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
return 0;
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend");
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0;
}
#endif
if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events");
"keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0;
}
@@ -263,9 +263,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
},
&owner_);
if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events");
"Start keyboard capturer failed, falling back to Slint keyboard events");
} else {
LOG_INFO("Start keyboard capturer with native hook");
}
@@ -273,9 +273,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}
int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend");
if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0;
}
+4
View File
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
// fullscreen action with an immediate, single-window fullscreen transition.
bool ConfigureStreamWindowLiveResize();
// Returns whether the configured stream window is currently the active AppKit
// key window.
bool IsStreamWindowActive();
// Enters or leaves the stream window's animation-free fullscreen mode.
bool SetStreamWindowFullscreen(bool fullscreen);
bool IsStreamWindowFullscreen();
+7
View File
@@ -146,6 +146,13 @@ bool ConfigureStreamWindowLiveResize() {
}
}
bool IsStreamWindowActive() {
@autoreleasepool {
return stream_window != nil && [NSApp isActive] &&
[stream_window isKeyWindow];
}
}
bool SetStreamWindowFullscreen(bool fullscreen) {
@autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) {
+6
View File
@@ -163,6 +163,7 @@ export component StreamWindow inherits Window {
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true;
private property <bool> display-menu-open: false;
@@ -486,6 +487,8 @@ export component StreamWindow inherits Window {
input-focus := FocusScope {
focus-on-click: true;
focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
accept
@@ -500,6 +503,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
input-focus.focus();
}
if event.kind == PointerEventKind.down
&& event.button == PointerEventButton.left
&& over-control {
+20
View File
@@ -187,6 +187,26 @@ int main() {
stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested);
bool keyboard_focus_changed = false;
bool keyboard_input_received = false;
stream->on_keyboard_focus_changed(
[&](bool focused) { keyboard_focus_changed = focused; });
stream->on_key_input(
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
keyboard_input_received = std::string(text) == "a" && pressed;
});
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
stream->window().dispatch_pointer_press_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_pointer_release_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_key_press_event("a");
assert(keyboard_focus_changed);
assert(keyboard_input_received);
crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip";
transfer.status = "Sending";