diff --git a/src/device_controller/keyboard/mac/keyboard_capturer.cpp b/src/device_controller/keyboard/mac/keyboard_capturer.cpp index dc6ac0d..d87c6b4 100644 --- a/src/device_controller/keyboard/mac/keyboard_capturer.cpp +++ b/src/device_controller/keyboard/mac/keyboard_capturer.cpp @@ -1,5 +1,6 @@ #include "keyboard_capturer.h" +#include #include #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 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); } diff --git a/src/device_controller/keyboard/windows/keyboard_capturer.cpp b/src/device_controller/keyboard/windows/keyboard_capturer.cpp index 7cd2b6e..8cfb4f3 100644 --- a/src/device_controller/keyboard/windows/keyboard_capturer.cpp +++ b/src/device_controller/keyboard/windows/keyboard_capturer.cpp @@ -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(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) { diff --git a/src/gui/application/application_state.h b/src/gui/application/application_state.h index ab5bbf1..1ac3ed9 100644 --- a/src/gui/application/application_state.h +++ b/src/gui/application/application_state.h @@ -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; diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp index 4fbd62c..40c8f48 100644 --- a/src/gui/application/gui_application.cpp +++ b/src/gui/application/gui_application.cpp @@ -30,6 +30,8 @@ #include "localization.h" #include "platform.h" #if _WIN32 +#include + #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(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); diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h index f23cb29..805460b 100644 --- a/src/gui/application/gui_application.h +++ b/src/gui/application/gui_application.h @@ -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(); diff --git a/src/gui/application/sdl_event_dispatch.cpp b/src/gui/application/sdl_event_dispatch.cpp index 9503529..ece1e20 100644 --- a/src/gui/application/sdl_event_dispatch.cpp +++ b/src/gui/application/sdl_event_dispatch.cpp @@ -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); diff --git a/src/gui/features/devices/session_device_manager.cpp b/src/gui/features/devices/session_device_manager.cpp index d7461db..e2fa37e 100644 --- a/src/gui/features/devices/session_device_manager.cpp +++ b/src/gui/features/devices/session_device_manager.cpp @@ -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; } diff --git a/src/gui/platform/window_drag.h b/src/gui/platform/window_drag.h index 5b1f328..da15d2a 100644 --- a/src/gui/platform/window_drag.h +++ b/src/gui/platform/window_drag.h @@ -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(); diff --git a/src/gui/platform/window_drag_mac.mm b/src/gui/platform/window_drag_mac.mm index 1cbbfe8..3522c13 100644 --- a/src/gui/platform/window_drag_mac.mm +++ b/src/gui/platform/window_drag_mac.mm @@ -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) { diff --git a/src/gui/ui/stream_window.slint b/src/gui/ui/stream_window.slint index 1e1cb1a..415806d 100644 --- a/src/gui/ui/stream_window.slint +++ b/src/gui/ui/stream_window.slint @@ -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 control-expanded: true; private property 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 { diff --git a/tests/slint_ui_smoke_test.cpp b/tests/slint_ui_smoke_test.cpp index 75ba9a1..a528289 100644 --- a/tests/slint_ui_smoke_test.cpp +++ b/tests/slint_ui_smoke_test.cpp @@ -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{1280.0f, 720.0f})); + stream->window().dispatch_pointer_press_event( + slint::LogicalPosition(slint::Point{640.0f, 360.0f}), + slint::PointerEventButton::Left); + stream->window().dispatch_pointer_release_event( + slint::LogicalPosition(slint::Point{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";