From 6898df7cdc6260a8a3642bd51030e1715967cae0 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 11 Nov 2026 00:24:31 +0800 Subject: [PATCH] [fix] align macOS and Linux remote window behavior --- apps/desktop/scripts/linux/package_deb.sh | 17 ++++ .../src/gui/application/gui_application.cpp | 10 +++ .../src/platform/macos/gui/tray/mac_tray.h | 2 + .../src/platform/macos/gui/tray/mac_tray.mm | 37 ++++++++- .../src/platform/macos/gui/window_drag_mac.mm | 6 ++ .../1.17.1-per-window-xdg-app-id.patch | 82 +++++++++++++++++++ deps/thirdparty/slint/xmake.lua | 9 ++ docs/USER_GUIDE.md | 4 + docs/USER_GUIDE_EN.md | 4 + 9 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 deps/thirdparty/slint/patches/1.17.1-per-window-xdg-app-id.patch diff --git a/apps/desktop/scripts/linux/package_deb.sh b/apps/desktop/scripts/linux/package_deb.sh index 121f277f..26fe702d 100755 --- a/apps/desktop/scripts/linux/package_deb.sh +++ b/apps/desktop/scripts/linux/package_deb.sh @@ -175,6 +175,23 @@ Type=Application Categories=Utility; EOF +# Give shells a desktop entry for the remote window's separate app id. Without +# it, some shells fall back to the process's main launcher and merge the groups; +# Wayland also uses this entry to resolve the icon. Hide it from app menus: a +# fresh launch can open CrossDesk, but cannot recreate a live remote session. +cat > "$DESKTOP_DIR/$PKG_NAME-remote-control.desktop" <tray) ui_->tray->SyncDockVisibility(); +#endif } void GuiApplication::PollUpdateCheck() { @@ -3854,6 +3859,11 @@ void GuiApplication::SyncStreamWindow() { if (!has_sessions) need_to_create_stream_window_ = false; if (need_to_create_stream_window_ && !ui_->stream) { ui_->stream.emplace(ui::StreamWindow::create()); +#if defined(__linux__) && !defined(__OHOS__) + // Set a per-window identity before Slint asynchronously creates the native + // window. The override survives hide/show and native handle recreation. + (*ui_->stream)->window().set_xdg_app_id("crossdesk-remote-control"); +#endif (*ui_->stream)->set_control_hand_cursor_supported( SupportsControlHandCursor(ui_->main->window())); #if (defined(__linux__) && !defined(__OHOS__)) diff --git a/apps/desktop/src/platform/macos/gui/tray/mac_tray.h b/apps/desktop/src/platform/macos/gui/tray/mac_tray.h index 2840f4b6..0b4bfa07 100644 --- a/apps/desktop/src/platform/macos/gui/tray/mac_tray.h +++ b/apps/desktop/src/platform/macos/gui/tray/mac_tray.h @@ -32,6 +32,8 @@ class MacTray { ~MacTray(); void MinimizeToTray(); + // Keep the Dock available while another normal window is open or minimized. + void SyncDockVisibility(); void RemoveTrayIcon(); bool TakeReopenRequest(); diff --git a/apps/desktop/src/platform/macos/gui/tray/mac_tray.mm b/apps/desktop/src/platform/macos/gui/tray/mac_tray.mm index aced47d7..28d04333 100644 --- a/apps/desktop/src/platform/macos/gui/tray/mac_tray.mm +++ b/apps/desktop/src/platform/macos/gui/tray/mac_tray.mm @@ -65,8 +65,30 @@ struct MacTrayImpl { } else if (app_window) { SDL_HideWindow(app_window); } - // Keep running through the menu-bar tray without a Dock application icon. - [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + SyncDockVisibility(); + } + + void SyncDockVisibility() { + // Cmd-H hides the application as a whole. Keep its Dock entry available + // so it can be unhidden; only closing individual windows enters tray mode. + if ([NSApp isHidden]) return; + + bool has_window = false; + for (NSWindow *window in [NSApp windows]) { + // A miniaturized NSWindow may no longer be eligible to become main, + // but it still needs a Dock entry from which the user can restore it. + if ([window isMiniaturized] || + ([window canBecomeMainWindow] && [window isVisible])) { + has_window = true; + break; + } + } + const NSApplicationActivationPolicy policy = has_window + ? NSApplicationActivationPolicyRegular + : NSApplicationActivationPolicyAccessory; + if ([NSApp activationPolicy] != policy) { + [NSApp setActivationPolicy:policy]; + } } void RemoveTrayIcon() { @@ -191,6 +213,14 @@ struct MacTrayImpl { return; } + // Unbundled development builds have no CFBundleIconFile, and winit's + // macOS window-icon setter is a no-op. Set the application icon explicitly + // so the Dock uses the logo instead of the generic executable icon. + NSImage *crossdesk_icon = LoadCrossDeskIcon(); + if (crossdesk_icon) { + [NSApp setApplicationIconImage:crossdesk_icon]; + } + status_item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; NSStatusBarButton *button = [status_item button]; @@ -200,7 +230,6 @@ struct MacTrayImpl { [button setToolTip:NSStringFromUtf8(tooltip)]; - NSImage *crossdesk_icon = LoadCrossDeskIcon(); if (crossdesk_icon) { NSImage *status_icon = [crossdesk_icon copy]; [status_icon setSize:NSMakeSize(18.0, 18.0)]; @@ -328,6 +357,8 @@ void MacActivateWindow(void *appkit_view) { void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); } +void MacTray::SyncDockVisibility() { impl_->SyncDockVisibility(); } + void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); } bool MacTray::TakeReopenRequest() { return impl_->TakeReopenRequest(); } diff --git a/apps/desktop/src/platform/macos/gui/window_drag_mac.mm b/apps/desktop/src/platform/macos/gui/window_drag_mac.mm index f1ac8d9a..3785b49c 100644 --- a/apps/desktop/src/platform/macos/gui/window_drag_mac.mm +++ b/apps/desktop/src/platform/macos/gui/window_drag_mac.mm @@ -174,6 +174,10 @@ bool ConfigureMainWindowChrome(void *opaque_slint_view, bool gallery) { NSButton *zoom_button = [window standardWindowButton:NSWindowZoomButton]; if (zoom_button == nil) return false; + // Keep the connection/DeskHub and remote-control windows independently + // switchable even when the user prefers opening documents in tabs. + window.tabbingMode = NSWindowTabbingModeDisallowed; + window.excludedFromWindowsMenu = NO; if (gallery) gallery_window = window; if (gallery && gallery_fullscreen) return true; if (gallery) @@ -280,6 +284,8 @@ bool ConfigureStreamWindowLiveResize(void *opaque_slint_view) { NSViewLayerContentsPlacementScaleProportionallyToFit; stream_window = window; stream_slint_view = slint_view; + window.tabbingMode = NSWindowTabbingModeDisallowed; + window.excludedFromWindowsMenu = NO; // Native Space fullscreen cross-fades an AppKit source snapshot over a // separately rendered destination window. Disable that path for the // stream window and route the green button to the immediate transition. diff --git a/deps/thirdparty/slint/patches/1.17.1-per-window-xdg-app-id.patch b/deps/thirdparty/slint/patches/1.17.1-per-window-xdg-app-id.patch new file mode 100644 index 00000000..72b386be --- /dev/null +++ b/deps/thirdparty/slint/patches/1.17.1-per-window-xdg-app-id.patch @@ -0,0 +1,82 @@ +--- a/internal/core/window.rs ++++ b/internal/core/window.rs +@@ -537,6 +537,7 @@ + close_requested: Callback<(), CloseRequestResponse>, + click_state: ClickState, + ctx: core::cell::OnceCell, ++ xdg_app_id_override: RefCell>, + } + + impl Drop for WindowInner { +@@ -598,6 +599,7 @@ + click_state: ClickState::default(), + prevent_focus_change: Default::default(), + ctx: Default::default(), ++ xdg_app_id_override: Default::default(), + menubar: Default::default(), + } + } +@@ -2170,9 +2172,15 @@ + } + } + +- /// Returns the (context global) xdg app id for use with wayland and x11. ++ /// Returns the window's xdg app id, falling back to the context default. + pub fn xdg_app_id(&self) -> Option { +- self.context().xdg_app_id() ++ self.xdg_app_id_override.borrow().clone().or_else(|| self.context().xdg_app_id()) ++ } ++ ++ /// Sets an override before native window creation. Winit reads this again ++ /// when recreating the native window, so hide/show preserves the identity. ++ pub fn set_xdg_app_id(&self, app_id: SharedString) { ++ self.xdg_app_id_override.replace(Some(app_id)); + } + + /// Returns the upgraded window adapter +@@ -2823,6 +2831,17 @@ + } + + #[unsafe(no_mangle)] ++ pub unsafe extern "C" fn slint_windowrc_set_xdg_app_id( ++ handle: *const WindowAdapterRcOpaque, ++ app_id: &SharedString, ++ ) { ++ unsafe { ++ let window_adapter = &*(handle as *const Rc); ++ WindowInner::from_pub(window_adapter.window()).set_xdg_app_id(app_id.clone()); ++ } ++ } ++ ++ #[unsafe(no_mangle)] + pub unsafe extern "C" fn slint_windowrc_set_fullscreen( + handle: *const WindowAdapterRcOpaque, + value: bool, +--- a/api/cpp/include/private/slint_window.h ++++ b/api/cpp/include/private/slint_window.h +@@ -497,6 +497,15 @@ + return inner.scale_factor(); + } + ++ /// Override the application id for this window on X11 and Wayland. ++ /// Call before native window creation; existing windows are not regrouped. ++ /// The override persists when the backend recreates the native window. ++ void set_xdg_app_id(const SharedString &app_id) ++ { ++ private_api::assert_main_thread(); ++ cbindgen_private::slint_windowrc_set_xdg_app_id(&inner.handle(), &app_id); ++ } ++ + /// Returns if the window is currently fullscreen + bool is_fullscreen() const + { +--- a/api/cpp/cbindgen.rs ++++ b/api/cpp/cbindgen.rs +@@ -756,6 +756,7 @@ + "slint_windowrc_dispatch_pointer_event", + "slint_windowrc_dispatch_key_event", + "slint_windowrc_dispatch_event", ++ "slint_windowrc_set_xdg_app_id", + "slint_windowrc_set_fullscreen", + "slint_windowrc_set_minimized", + "slint_windowrc_set_maximized", diff --git a/deps/thirdparty/slint/xmake.lua b/deps/thirdparty/slint/xmake.lua index 1c8224e5..08e0c567 100644 --- a/deps/thirdparty/slint/xmake.lua +++ b/deps/thirdparty/slint/xmake.lua @@ -7,6 +7,15 @@ package("slint") "https://github.com/slint-ui/slint.git") add_versions("1.17.1", "68222567f8c70ff677cd4a98cd94fb4765ac0f797eb8f8608a646911c908dc2a") + if is_plat("linux") then + -- Slint 1.17.1 only exposes a context-wide xdg app id. Keep a window + -- override through deferred native creation and hide/show on both + -- X11 and Wayland. Include the extension in the package cache key. + add_configs("per_window_xdg_app_id", {description = "Support per-window XDG application IDs", default = true, type = "boolean", readonly = true}) + add_patches("1.17.1", path.join(os.scriptdir(), "patches/1.17.1-per-window-xdg-app-id.patch"), + "86aedd42960c6e1b0f9d2a37ee5681bff177f166ee7aa77fee4b18c72fbff3d3") + end + add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true}) if is_plat("windows") then -- Include CRT linkage in the package hash so cached /MD builds are not reused. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 0961d3f8..7e97675c 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -129,6 +129,10 @@ sudo apt install "./crossdesk-virtual-desktop-xfce-linux-amd64-.deb" Windows 任务栏将主窗口与远程控制窗口显示为两个独立图标,可分别切换和最小化。关闭主窗口后,远程控制窗口及其连接继续保留;需要主窗口时从系统托盘重新打开。远程控制窗口的任务栏图标仅在会话窗口存在时显示,固定应用请使用主窗口的图标。 +Linux 的主窗口与远程控制窗口使用不同的应用分组标识,支持 X11、XWayland 和原生 Wayland;实际任务栏显示方式由桌面环境及分组设置决定。关闭主窗口不影响远控连接,可从系统托盘重新打开主窗口。固定应用请使用主窗口的启动器。 + +macOS 保留一个 CrossDesk Dock 应用图标,主窗口与远程控制窗口可独立切换和最小化,不会自动合并为系统窗口标签页。关闭主窗口后,只要还有可见或已最小化的窗口,Dock 图标就会保留,远控连接继续运行;所有窗口关闭后,应用保留在菜单栏中。可从菜单栏重新打开主窗口。 + 桌面端普通会话、桌面墙和移动端(Android、iOS、鸿蒙)的网络状态均显示“被控端编码器”和“控制端解码器”,使用实际的视频编解码器名称,包括回退后的软件编解码器。名称保持单行,超出可用宽度时自动横向滚动,完整显示时保持静止。名称统一采用“实现名称 (编码格式)”,名称首字母大写、技术缩写保留全大写,例如 `VideoToolbox (H.264)`、`NVENC (H.264)`、`NVDEC (H.264)`、`SVT-AV1 (AV1)`、`Dav1d (AV1)` 和 `LibAOM (AV1)`;Media Foundation 额外标注 `HW`(硬件)或 `SW`(软件)。被控端需要支持编码器状态上报;旧版本或尚未获取到的信息显示“—”。 ### 在会话中调整画面 diff --git a/docs/USER_GUIDE_EN.md b/docs/USER_GUIDE_EN.md index 6733c262..d947d3d2 100644 --- a/docs/USER_GUIDE_EN.md +++ b/docs/USER_GUIDE_EN.md @@ -129,6 +129,10 @@ Desktop clients synchronize text clipboard contents. Configure the receiving dir On Windows, the main window and remote control window use separate taskbar buttons, so you can switch to or minimize either window independently. Closing the main window leaves the remote control window and its connections running; reopen the main window from the system tray. The remote control button appears only while the session window is present; use the main window's button to pin the application. +On Linux, the main and remote control windows use different application grouping IDs on X11, XWayland, and native Wayland. Taskbar presentation depends on the desktop environment and its grouping settings. Closing the main window keeps remote connections running; reopen it from the system tray. Use the main application's launcher for pinning. + +On macOS, CrossDesk keeps one application icon in the Dock. The main and remote control windows can be switched and minimized independently and do not automatically merge into native window tabs. Closing the main window keeps the Dock icon while another window is visible or minimized, and remote connections continue running. Once all windows are closed, the application remains in the menu bar, where you can reopen the main window. + Network statistics in both ordinary sessions and DeskHub show the video codecs actually in use, including software fallback. Names use “Implementation (Format)” with an initial capital and uppercase technical abbreviations, such as `VideoToolbox (H.264)`, `NVENC (H.264)`, `NVDEC (H.264)`, `SVT-AV1 (AV1)`, `Dav1d (AV1)`, and `LibAOM (AV1)`; Media Foundation also includes `HW` (hardware) or `SW` (software). The host must support encoder status reporting; older versions and information not yet available display “—”. ### Adjust video during a session