diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index adc153df..d2503ba6 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -48,6 +48,7 @@ #include "features/gallery/gallery_workspace.h" #include "features/gallery/preview_bandwidth.h" #include "features/gallery/preview_resolution.h" +#include "features/input/stream_keyboard_focus.h" #include "filesystem_utf8.h" #include "localization.h" #include "platform.h" @@ -3803,19 +3804,10 @@ void GuiApplication::SyncXWaylandWindowActivation() { #endif void GuiApplication::SetStreamKeyboardFocus(bool focused) { - // Local dialogs own input even while the native window is active. - if (focused && ui_ && ui_->stream && - (*ui_->stream)->get_modal_visible()) { - focused = false; - } - if (focus_on_stream_window_ == focused) { - return; - } - - focus_on_stream_window_ = focused; - if (!focused) { - ReleaseControl(); - } + const bool local_input_active = ui_ && ui_->stream && + (*ui_->stream)->get_local_input_active(); + UpdateStreamKeyboardFocus(focused, local_input_active, focus_on_stream_window_, + [this](bool preserve_local_ui) { ReleaseControl(preserve_local_ui); }); } void GuiApplication::SyncStreamKeyboardFocus() { @@ -5665,7 +5657,7 @@ void GuiApplication::SyncAudioPlayback() { } } -void GuiApplication::ReleasePressedInput() { +void GuiApplication::ReleasePressedInput(bool preserve_local_ui) { keyboard_.ForceReleasePressedKeys(); for (int button : ui_->pressed_mouse_buttons) { RemoteAction action{}; @@ -5678,13 +5670,16 @@ void GuiApplication::ReleasePressedInput() { SendRemoteInput(action); } ui_->pressed_mouse_buttons.clear(); - if (ui_->stream) (*ui_->stream)->invoke_reset_input_state(); + if (ui_->stream) { + if (preserve_local_ui) (*ui_->stream)->invoke_reset_pressed_input(); + else (*ui_->stream)->invoke_reset_input_state(); + } } -void GuiApplication::ReleaseControl() { +void GuiApplication::ReleaseControl(bool preserve_local_ui) { const auto gallery_target = ui_->gallery_control_id; devices_.StopKeyboardCapturer(); - ReleasePressedInput(); + ReleasePressedInput(preserve_local_ui); gallery_group_control_id_.clear(); ui_->wall->set_gallery_group_controlling(false); ui_->wall->set_gallery_remote_cursor_shape( diff --git a/apps/desktop/src/gui/application/gui_application.h b/apps/desktop/src/gui/application/gui_application.h index 065764e5..b4ecd553 100644 --- a/apps/desktop/src/gui/application/gui_application.h +++ b/apps/desktop/src/gui/application/gui_application.h @@ -84,8 +84,8 @@ private: void DeviceGroupAction(const std::string& action, const std::string& value, bool gallery_window); bool SaveDeviceGroups(const std::string& new_group, bool gallery_window); - void ReleasePressedInput(); - void ReleaseControl(); + void ReleasePressedInput(bool preserve_local_ui = false); + void ReleaseControl(bool preserve_local_ui = false); void SyncAudioPlayback(); void SaveGalleryWindowState(); void SyncAnnouncements(); diff --git a/apps/desktop/src/gui/features/input/stream_keyboard_focus.h b/apps/desktop/src/gui/features/input/stream_keyboard_focus.h new file mode 100644 index 00000000..41bb171f --- /dev/null +++ b/apps/desktop/src/gui/features/input/stream_keyboard_focus.h @@ -0,0 +1,26 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-11-11 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _STREAM_KEYBOARD_FOCUS_H_ +#define _STREAM_KEYBOARD_FOCUS_H_ + +namespace crossdesk { + +// Native window focus and ownership of keyboard input are different: a local +// menu keeps the window active while temporarily suspending remote input. +// Publish the new state before releasing input, since UI callbacks can reenter. +template +void UpdateStreamKeyboardFocus(bool focused, bool local_input_active, + bool& remote_focused, Release release) { + focused = focused && !local_input_active; + if (remote_focused == focused) return; + remote_focused = focused; + if (!focused) release(local_input_active); +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 7329e177..adb5ce9d 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -423,6 +423,10 @@ export component StreamWindow inherits Window { private property disconnect-menu-y: root.control-panel-y(root.disconnect-menu-height); private property control-menu-open: root.display-menu-open || root.shortcut-menu-open || root.video-menu-open || root.disconnect-menu-open; + out property local-input-active: root.modal-visible || root.control-menu-open; + changed control-menu-open => { + root.keyboard-focus-changed(!root.control-menu-open && !root.modal-visible && input-focus.has-focus); + } changed selected-tab => { root.close-control-menus(); root.cancel-remote-pointer(); @@ -589,10 +593,14 @@ export component StreamWindow inherits Window { // Native focus/session changes release input in C++; clear the matching UI // ownership too, including when replacing the only tab (same tab index). - public function reset-input-state() { + public function reset-pressed-input() { root.remote-left-down = false; root.remote-right-down = false; root.remote-middle-down = false; + } + + public function reset-input-state() { + root.reset-pressed-input(); root.close-control-menus(); root.control-drag-pending = false; root.end-control-drag(); @@ -1039,10 +1047,10 @@ export component StreamWindow inherits Window { input-focus := FocusScope { focus-on-click: true; - focus-gained => { root.keyboard-focus-changed(true); } + focus-gained => { root.keyboard-focus-changed(!root.control-menu-open && !root.modal-visible); } focus-lost => { root.keyboard-focus-changed(false); } key-pressed(event) => { - if !root.modal-visible { + if !root.modal-visible && !root.control-menu-open { root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } accept @@ -1053,7 +1061,7 @@ export component StreamWindow inherits Window { if event.text == Key.Escape || event.text == Key.Return { if root.privacy-unlock-visible { root.dismiss-privacy-unlock(); } else { root.dismiss-remote-update(); } } - } else { + } else if !root.control-menu-open { root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); } accept