From 76b475450beb606eb181d380d4cd3050cc19849b Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 26 Nov 2025 18:18:40 +0800 Subject: [PATCH] [feat] request macOS system permissions by showing a prompt on startup --- .gitignore | 4 +- src/gui/assets/layouts/layout.h | 5 +- src/gui/assets/localization/localization.h | 21 +++ src/gui/render.cpp | 6 + src/gui/render.h | 11 ++ src/gui/windows/request_permission_window.mm | 184 +++++++++++++++++++ xmake.lua | 2 + 7 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 src/gui/windows/request_permission_window.mm diff --git a/.gitignore b/.gitignore index f3808b5..379cb48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ # Xmake cache .xmake/ build/ +certs/ # MacOS Cache .DS_Store # VSCode cache -.vscode -continuous-desk.code-workspace \ No newline at end of file +.vscode \ No newline at end of file diff --git a/src/gui/assets/layouts/layout.h b/src/gui/assets/layouts/layout.h index d35d8cc..f2af7d5 100644 --- a/src/gui/assets/layouts/layout.h +++ b/src/gui/assets/layouts/layout.h @@ -75,5 +75,8 @@ #define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91 #define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162 #define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146 - +#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 180 +#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 128 +#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 380 +#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 122 #endif \ No newline at end of file diff --git a/src/gui/assets/localization/localization.h b/src/gui/assets/localization/localization.h index 6baea87..aa9dc7a 100644 --- a/src/gui/assets/localization/localization.h +++ b/src/gui/assets/localization/localization.h @@ -188,6 +188,27 @@ static std::vector minimize_to_tray = { "Minimize to system tray when exit:"}; static std::vector exit_program = {L"退出", L"Exit"}; #endif +#ifdef __APPLE__ +static std::vector request_permissions = { + reinterpret_cast(u8"权限请求"), "Request Permissions"}; +static std::vector screen_recording_permission = { + reinterpret_cast(u8"录屏权限"), "Screen Recording Permission"}; +static std::vector accessibility_permission = { + reinterpret_cast(u8"键鼠权限"), "Keyboard & Mouse Permission"}; +static std::vector permission_granted = { + reinterpret_cast(u8"已授权"), "Granted"}; +static std::vector permission_denied = { + reinterpret_cast(u8"未授权"), "Denied"}; +static std::vector open_screen_recording_settings = { + reinterpret_cast(u8"打开录屏设置"), + "Open Screen Recording Settings"}; +static std::vector open_keyboard_mouse_settings = { + reinterpret_cast(u8"打开键鼠设置"), + "Open Keyboard & Mouse Settings"}; +static std::vector permission_required_message = { + reinterpret_cast(u8"应用需要以下权限才能正常工作:"), + "The application requires the following permissions to work properly:"}; +#endif } // namespace localization } // namespace crossdesk #endif \ No newline at end of file diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 2b4c990..e09a421 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -895,6 +895,12 @@ int Render::DrawMainWindow() { UpdateNotificationWindow(); +#ifdef __APPLE__ + if (show_request_permission_window_) { + RequestPermissionWindow(); + } +#endif + ImGui::End(); // Rendering diff --git a/src/gui/render.h b/src/gui/render.h index 74343fe..af76e74 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -188,6 +188,14 @@ class Render { int NetTrafficStats(std::shared_ptr& props); void DrawConnectionStatusText( std::shared_ptr& props); +#ifdef __APPLE__ + int RequestPermissionWindow(); + bool CheckScreenRecordingPermission(); + bool CheckAccessibilityPermission(); + void OpenSystemPreferences(); + void OpenScreenRecordingPreferences(); + void OpenAccessibilityPreferences(); +#endif public: static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame, @@ -449,6 +457,9 @@ class Render { bool show_new_version_icon_in_menu_ = true; uint64_t new_version_icon_last_trigger_time_ = 0; uint64_t new_version_icon_render_start_time_ = 0; +#ifdef __APPLE__ + bool show_request_permission_window_ = true; +#endif char client_id_[10] = ""; char client_id_display_[12] = ""; char client_id_with_password_[17] = ""; diff --git a/src/gui/windows/request_permission_window.mm b/src/gui/windows/request_permission_window.mm new file mode 100644 index 0000000..6fc4ee3 --- /dev/null +++ b/src/gui/windows/request_permission_window.mm @@ -0,0 +1,184 @@ +#include "layout.h" +#include "localization.h" +#include "rd_log.h" +#include "render.h" + +#ifdef __APPLE__ +#include +#include +#import +#include +#include +#endif + +namespace crossdesk { + +#ifdef __APPLE__ +bool Render::CheckScreenRecordingPermission() { + // CGPreflightScreenCaptureAccess is available on macOS 10.15+ + if (@available(macOS 10.15, *)) { + bool granted = CGPreflightScreenCaptureAccess(); + LOG_INFO("CGPreflightScreenCaptureAccess returned: {}", granted); + return granted; + } + // For older macOS versions, assume permission is granted + return true; +} + +bool Render::CheckAccessibilityPermission() { + // Check if the process is trusted for accessibility + // Note: This may require app restart to reflect permission changes + NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO}; + bool trusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); + LOG_INFO("AXIsProcessTrustedWithOptions returned: {}", trusted); + return trusted; +} + +void Render::OpenSystemPreferences() { + // Open System Preferences to the Privacy & Security > Screen Recording or + // Accessibility section + system("open " + "\"x-apple.systempreferences:com.apple.preference.security?Privacy_" + "ScreenCapture\""); +} + +void Render::OpenScreenRecordingPreferences() { + // Request screen recording permission first to ensure app appears in System Settings + if (@available(macOS 10.15, *)) { + CGRequestScreenCaptureAccess(); + } + // Open System Preferences to the Privacy & Security > Screen Recording section + system("open " + "\"x-apple.systempreferences:com.apple.preference.security?Privacy_" + "ScreenCapture\""); +} + +void Render::OpenAccessibilityPreferences() { + // Request accessibility permission first to ensure app appears in System Settings + NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES}; + AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); + // Open System Preferences to the Privacy & Security > Accessibility section + system("open " + "\"x-apple.systempreferences:com.apple.preference.security?Privacy_" + "Accessibility\""); +} +#endif + +int Render::RequestPermissionWindow() { +#ifdef __APPLE__ + // Check permissions - recheck every frame to update status immediately after user grants + // permission + bool screen_recording_granted = CheckScreenRecordingPermission(); + bool accessibility_granted = CheckAccessibilityPermission(); + + // Update show_request_permission_window_ based on permission status + // Keep window visible if any permission is not granted + show_request_permission_window_ = !screen_recording_granted || !accessibility_granted; + + // Log permission status for debugging + LOG_INFO("Screen recording permission: {}, Accessibility permission: {}", + screen_recording_granted, accessibility_granted); + + if (!show_request_permission_window_) { + LOG_INFO("Request permission window is not shown"); + return 0; + } + LOG_INFO("Request permission window is shown"); + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + float window_width = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_WIDTH_CN + : REQUEST_PERMISSION_WINDOW_WIDTH_EN; + float window_height = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_HEIGHT_CN + : REQUEST_PERMISSION_WINDOW_HEIGHT_EN; + + // Center the window on screen + ImVec2 center_pos = ImVec2((viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x, + (viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y); + ImGui::SetNextWindowPos(center_pos, ImGuiCond_Always); + + ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always); + + // Make window always on top and modal-like + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f, 15.0f)); + + ImGui::Begin(localization::request_permissions[localization_language_index_].c_str(), nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_Modal); + + ImGui::SetWindowFontScale(0.3f); + + // use system Chinese font + if (system_chinese_font_ != nullptr) { + ImGui::PushFont(system_chinese_font_); + } + + // Message + ImGui::SetCursorPosX(10.0f); + ImGui::TextWrapped( + "%s", localization::permission_required_message[localization_language_index_].c_str()); + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + + // Accessibility Permission + ImGui::SetCursorPosX(10.0f); + ImGui::AlignTextToFramePadding(); + ImGui::Text("1. %s:", + localization::accessibility_permission[localization_language_index_].c_str()); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + if (accessibility_granted) { + ImGui::Text("%s", localization::permission_granted[localization_language_index_].c_str()); + } else { + ImGui::Text("%s", localization::permission_denied[localization_language_index_].c_str()); + ImGui::SameLine(); + if (ImGui::Button( + localization::open_keyboard_mouse_settings[localization_language_index_].c_str())) { + OpenAccessibilityPreferences(); + } + } + + ImGui::Spacing(); + + // Screen Recording Permission + ImGui::SetCursorPosX(10.0f); + ImGui::AlignTextToFramePadding(); + ImGui::Text("2. %s:", + localization::screen_recording_permission[localization_language_index_].c_str()); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + if (screen_recording_granted) { + ImGui::Text("%s", localization::permission_granted[localization_language_index_].c_str()); + } else { + ImGui::Text("%s", localization::permission_denied[localization_language_index_].c_str()); + ImGui::SameLine(); + if (ImGui::Button( + localization::open_screen_recording_settings[localization_language_index_].c_str())) { + OpenScreenRecordingPreferences(); + } + } + + ImGui::SetWindowFontScale(1.0f); + ImGui::SetWindowFontScale(0.45f); + + // pop system font + if (system_chinese_font_ != nullptr) { + ImGui::PopFont(); + } + + ImGui::End(); + ImGui::SetWindowFontScale(1.0f); + ImGui::PopStyleVar(4); + ImGui::PopStyleColor(); + + return 0; +#else + return 0; +#endif +} +} // namespace crossdesk \ No newline at end of file diff --git a/xmake.lua b/xmake.lua index 93aca38..7de596a 100644 --- a/xmake.lua +++ b/xmake.lua @@ -182,6 +182,8 @@ target("gui") if is_os("windows") then add_files("src/gui/tray/*.cpp") add_includedirs("src/gui/tray", {public = true}) + elseif is_os("macosx") then + add_files("src/gui/windows/*.mm") end target("crossdesk")