From ae5950fe5745d1d61e1808fc9edc38d49e24ca30 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sun, 23 Aug 2026 14:30:53 +0200 Subject: [PATCH 1/4] feat: add setWillCloseHook to WindowManager Dart API Exposes the native WindowWillCloseHook through the Dart FFI layer, matching the existing setWillShowHook/setWillHideHook API surface. Changes: - cnativeapi/bindings_generated.dart: FFI bindings for native_window_manager_set_will_close_hook, has_will_close_hook, handle_will_close, call_original_close, plus the callback typedef. - nativeapi/window_manager.dart: Dart methods setWillCloseHook, hasWillCloseHook, handleWillClose, callOriginalClose. Depends on libnativeapi/nativeapi#51 (C++ SetWillCloseHook). --- .../lib/src/bindings_generated.dart | 71 +++++++++++++++++++ .../nativeapi/lib/src/window_manager.dart | 23 ++++++ 2 files changed, 94 insertions(+) diff --git a/packages/cnativeapi/lib/src/bindings_generated.dart b/packages/cnativeapi/lib/src/bindings_generated.dart index 05ecfa0..33ee06a 100644 --- a/packages/cnativeapi/lib/src/bindings_generated.dart +++ b/packages/cnativeapi/lib/src/bindings_generated.dart @@ -4358,6 +4358,67 @@ class CNativeApiBindings { _native_window_manager_call_original_hidePtr .asFunction(); + void native_window_manager_set_will_close_hook( + native_window_manager_set_will_close_hook_callback_t hook, + ffi.Pointer hook_user_data, + ) { + return _native_window_manager_set_will_close_hook(hook, hook_user_data); + } + + late final _native_window_manager_set_will_close_hookPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + native_window_manager_set_will_close_hook_callback_t, + ffi.Pointer, + ) + > + >('native_window_manager_set_will_close_hook'); + late final _native_window_manager_set_will_close_hook = + _native_window_manager_set_will_close_hookPtr + .asFunction< + void Function( + native_window_manager_set_will_close_hook_callback_t, + ffi.Pointer, + ) + >(); + + bool native_window_manager_has_will_close_hook() { + return _native_window_manager_has_will_close_hook(); + } + + late final _native_window_manager_has_will_close_hookPtr = + _lookup>( + 'native_window_manager_has_will_close_hook', + ); + late final _native_window_manager_has_will_close_hook = + _native_window_manager_has_will_close_hookPtr + .asFunction(); + + void native_window_manager_handle_will_close(int id) { + return _native_window_manager_handle_will_close(id); + } + + late final _native_window_manager_handle_will_closePtr = + _lookup>( + 'native_window_manager_handle_will_close', + ); + late final _native_window_manager_handle_will_close = + _native_window_manager_handle_will_closePtr + .asFunction(); + + bool native_window_manager_call_original_close(int id) { + return _native_window_manager_call_original_close(id); + } + + late final _native_window_manager_call_original_closePtr = + _lookup>( + 'native_window_manager_call_original_close', + ); + late final _native_window_manager_call_original_close = + _native_window_manager_call_original_closePtr + .asFunction(); + /// Registers @p callback for every WindowEvent this WindowManager emits. /// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. int native_window_manager_add_listener( @@ -5399,6 +5460,16 @@ typedef native_window_manager_set_will_hide_hook_callback_tFunction = ffi.Void Function(ffi.UnsignedInt arg0, ffi.Pointer user_data); typedef Dartnative_window_manager_set_will_hide_hook_callback_tFunction = void Function(int arg0, ffi.Pointer user_data); +typedef native_window_manager_set_will_close_hook_callback_t = + ffi.Pointer< + ffi.NativeFunction< + native_window_manager_set_will_close_hook_callback_tFunction + > + >; +typedef native_window_manager_set_will_close_hook_callback_tFunction = + ffi.Void Function(ffi.UnsignedInt arg0, ffi.Pointer user_data); +typedef Dartnative_window_manager_set_will_close_hook_callback_tFunction = + void Function(int arg0, ffi.Pointer user_data); typedef native_window_event_callback_t = ffi.Pointer>; typedef native_window_event_callback_tFunction = diff --git a/packages/nativeapi/lib/src/window_manager.dart b/packages/nativeapi/lib/src/window_manager.dart index 1c4e431..ff008f6 100644 --- a/packages/nativeapi/lib/src/window_manager.dart +++ b/packages/nativeapi/lib/src/window_manager.dart @@ -69,6 +69,17 @@ class WindowManager { _bindings.native_window_manager_set_will_hide_hook(hookCallable?.nativeFunction ?? ffi.nullptr, ffi.nullptr); } + void setWillCloseHook(void Function(int)? hook) { + final hookCallable = hook == null ? null : ffi.NativeCallable< + ffi.Void Function(ffi.UnsignedInt, ffi.Pointer)>.isolateLocal( + (int arg0, ffi.Pointer _) { + hook(arg0); + }, + ); + if (hookCallable != null) _listeners.add(hookCallable); + _bindings.native_window_manager_set_will_close_hook(hookCallable?.nativeFunction ?? ffi.nullptr, ffi.nullptr); + } + bool hasWillShowHook() { return _bindings.native_window_manager_has_will_show_hook(); } @@ -77,6 +88,10 @@ class WindowManager { return _bindings.native_window_manager_has_will_hide_hook(); } + bool hasWillCloseHook() { + return _bindings.native_window_manager_has_will_close_hook(); + } + void handleWillShow(WindowId id) { _bindings.native_window_manager_handle_will_show(id); } @@ -85,6 +100,10 @@ class WindowManager { _bindings.native_window_manager_handle_will_hide(id); } + void handleWillClose(WindowId id) { + _bindings.native_window_manager_handle_will_close(id); + } + bool callOriginalShow(WindowId id) { return _bindings.native_window_manager_call_original_show(id); } @@ -93,6 +112,10 @@ class WindowManager { return _bindings.native_window_manager_call_original_hide(id); } + bool callOriginalClose(WindowId id) { + return _bindings.native_window_manager_call_original_close(id); + } + /// Registers [callback] for every `WindowEvent` this `WindowManager` emits. /// /// The callback runs synchronously on whichever thread the native side From d9c3c2917d68a7ea654608f8d9ae3013df5c721d Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sun, 23 Aug 2026 21:06:57 +0200 Subject: [PATCH 2/4] fix: vendor cxx_impl directly instead of git submodule pub (Flutter's package manager) does not initialize git submodules when fetching git dependencies. This left cxx_impl/ empty in the pub cache, causing iOS and macOS builds to fail with "file not found" on the C++ source files. Vendoring the C++ code directly into packages/cnativeapi/cxx_impl/ removes the submodule dependency and makes the package self-contained. --- .gitmodules | 3 - packages/cnativeapi/cxx_impl | 1 - packages/cnativeapi/cxx_impl/.clang-format | 10 + .../cxx_impl/.cursor/rules/c-api-bindings.mdc | 555 +++++++++ .../cxx_impl/.cursor/rules/event-system.mdc | 337 ++++++ .../.cursor/rules/native-object-provider.mdc | 519 ++++++++ .../cxx_impl/.cursor/rules/pimpl-pattern.mdc | 434 +++++++ .../.cursor/rules/platform-implementation.mdc | 634 ++++++++++ .../.cursor/rules/project-architecture.mdc | 184 +++ .../.cursor/rules/singleton-managers.mdc | 404 +++++++ .../cxx_impl/.github/workflows/build.yml | 253 ++++ packages/cnativeapi/cxx_impl/.gitignore | 19 + packages/cnativeapi/cxx_impl/CMakeLists.txt | 35 + packages/cnativeapi/cxx_impl/LICENSE | 21 + packages/cnativeapi/cxx_impl/README.md | 82 ++ .../application_c_example/CMakeLists.txt | 28 + .../examples/application_c_example/main.c | 74 ++ .../application_example/CMakeLists.txt | 28 + .../examples/application_example/main.cpp | 73 ++ .../examples/display_c_example/CMakeLists.txt | 13 + .../examples/display_c_example/main.c | 108 ++ .../examples/display_example/CMakeLists.txt | 28 + .../examples/display_example/main.cpp | 116 ++ .../id_allocator_example/CMakeLists.txt | 11 + .../examples/id_allocator_example/main.cpp | 164 +++ .../examples/keyboard_example/CMakeLists.txt | 19 + .../examples/keyboard_example/main.cpp | 115 ++ .../launch_at_login_c_example/CMakeLists.txt | 15 + .../examples/launch_at_login_c_example/main.c | 129 ++ .../launch_at_login_example/CMakeLists.txt | 16 + .../examples/launch_at_login_example/main.cpp | 101 ++ .../examples/menu_c_example/CMakeLists.txt | 30 + .../cxx_impl/examples/menu_c_example/main.c | 221 ++++ .../examples/menu_example/CMakeLists.txt | 30 + .../cxx_impl/examples/menu_example/main.cpp | 167 +++ .../message_dialog_c_example/CMakeLists.txt | 29 + .../examples/message_dialog_c_example/main.c | 148 +++ .../message_dialog_example/CMakeLists.txt | 29 + .../examples/message_dialog_example/main.cpp | 111 ++ .../shortcut_c_example/CMakeLists.txt | 14 + .../examples/shortcut_c_example/main.c | 232 ++++ .../examples/shortcut_example/CMakeLists.txt | 20 + .../examples/shortcut_example/main.cpp | 236 ++++ .../examples/storage_c_example/CMakeLists.txt | 34 + .../examples/storage_c_example/main.c | 164 +++ .../examples/storage_example/CMakeLists.txt | 34 + .../examples/storage_example/main.cpp | 128 ++ .../tray_icon_c_example/CMakeLists.txt | 28 + .../examples/tray_icon_c_example/main.c | 229 ++++ .../examples/tray_icon_example/CMakeLists.txt | 28 + .../examples/tray_icon_example/main.cpp | 171 +++ .../url_opener_c_example/CMakeLists.txt | 12 + .../examples/url_opener_c_example/main.c | 27 + .../examples/window_c_example/CMakeLists.txt | 28 + .../cxx_impl/examples/window_c_example/main.c | 467 ++++++++ .../examples/window_example/CMakeLists.txt | 28 + .../cxx_impl/examples/window_example/main.cpp | 334 ++++++ .../cnativeapi/cxx_impl/include/nativeapi.h | 60 + .../cnativeapi/cxx_impl/src/CMakeLists.txt | 94 ++ .../cxx_impl/src/accessibility_manager.cpp | 14 + .../cxx_impl/src/accessibility_manager.h | 91 ++ .../cnativeapi/cxx_impl/src/application.cpp | 14 + .../cnativeapi/cxx_impl/src/application.h | 309 +++++ .../src/capi/accessibility_manager_c.cpp | 36 + .../src/capi/accessibility_manager_c.h | 29 + .../cxx_impl/src/capi/application_c.cpp | 205 ++++ .../cxx_impl/src/capi/application_c.h | 105 ++ .../cnativeapi/cxx_impl/src/capi/color_c.cpp | 77 ++ .../cnativeapi/cxx_impl/src/capi/color_c.h | 98 ++ .../cnativeapi/cxx_impl/src/capi/common_c.h | 27 + .../cnativeapi/cxx_impl/src/capi/dialog_c.cpp | 17 + .../cnativeapi/cxx_impl/src/capi/dialog_c.h | 66 + .../cxx_impl/src/capi/display_c.cpp | 235 ++++ .../cnativeapi/cxx_impl/src/capi/display_c.h | 174 +++ .../cxx_impl/src/capi/display_manager_c.cpp | 91 ++ .../cxx_impl/src/capi/display_manager_c.h | 44 + .../cxx_impl/src/capi/geometry_c.cpp | 17 + .../cnativeapi/cxx_impl/src/capi/geometry_c.h | 101 ++ .../cnativeapi/cxx_impl/src/capi/image_c.cpp | 106 ++ .../cnativeapi/cxx_impl/src/capi/image_c.h | 66 + .../cxx_impl/src/capi/keyboard_c.cpp | 75 ++ .../cnativeapi/cxx_impl/src/capi/keyboard_c.h | 164 +++ .../cxx_impl/src/capi/keyboard_monitor_c.cpp | 111 ++ .../cxx_impl/src/capi/keyboard_monitor_c.h | 62 + .../cxx_impl/src/capi/launch_at_login_c.cpp | 185 +++ .../cxx_impl/src/capi/launch_at_login_c.h | 85 ++ .../cnativeapi/cxx_impl/src/capi/menu_c.cpp | 706 +++++++++++ .../cnativeapi/cxx_impl/src/capi/menu_c.h | 360 ++++++ .../cxx_impl/src/capi/message_dialog_c.cpp | 142 +++ .../cxx_impl/src/capi/message_dialog_c.h | 70 ++ .../cxx_impl/src/capi/placement_c.cpp | 17 + .../cxx_impl/src/capi/placement_c.h | 111 ++ .../src/capi/positioning_strategy_c.cpp | 136 +++ .../src/capi/positioning_strategy_c.h | 112 ++ .../cxx_impl/src/capi/preferences_c.cpp | 164 +++ .../cxx_impl/src/capi/preferences_c.h | 77 ++ .../cxx_impl/src/capi/secure_storage_c.cpp | 173 +++ .../cxx_impl/src/capi/secure_storage_c.h | 80 ++ .../cxx_impl/src/capi/shortcut_c.cpp | 246 ++++ .../cnativeapi/cxx_impl/src/capi/shortcut_c.h | 212 ++++ .../cxx_impl/src/capi/shortcut_manager_c.cpp | 216 ++++ .../cxx_impl/src/capi/shortcut_manager_c.h | 84 ++ .../cxx_impl/src/capi/string_utils_c.cpp | 90 ++ .../cxx_impl/src/capi/string_utils_c.h | 94 ++ .../cxx_impl/src/capi/tray_icon_c.cpp | 370 ++++++ .../cxx_impl/src/capi/tray_icon_c.h | 220 ++++ .../cxx_impl/src/capi/tray_manager_c.cpp | 60 + .../cxx_impl/src/capi/tray_manager_c.h | 34 + .../cxx_impl/src/capi/url_opener_c.cpp | 55 + .../cxx_impl/src/capi/url_opener_c.h | 118 ++ .../cnativeapi/cxx_impl/src/capi/window_c.cpp | 1066 +++++++++++++++++ .../cnativeapi/cxx_impl/src/capi/window_c.h | 391 ++++++ .../cxx_impl/src/capi/window_manager_c.cpp | 213 ++++ .../cxx_impl/src/capi/window_manager_c.h | 86 ++ packages/cnativeapi/cxx_impl/src/dialog.cpp | 15 + packages/cnativeapi/cxx_impl/src/dialog.h | 136 +++ packages/cnativeapi/cxx_impl/src/display.h | 190 +++ .../cxx_impl/src/display_manager.cpp | 76 ++ .../cnativeapi/cxx_impl/src/display_manager.h | 152 +++ .../cxx_impl/src/foundation/color.cpp | 86 ++ .../cxx_impl/src/foundation/color.h | 101 ++ .../cxx_impl/src/foundation/dispatcher.cpp | 59 + .../cxx_impl/src/foundation/dispatcher.h | 125 ++ .../src/foundation/dispatcher_common.h | 31 + .../src/foundation/dispatcher_platform.h | 25 + .../cxx_impl/src/foundation/event.h | 100 ++ .../cxx_impl/src/foundation/event_emitter.h | 592 +++++++++ .../cxx_impl/src/foundation/geometry.h | 31 + .../cxx_impl/src/foundation/handle_table.cpp | 114 ++ .../cxx_impl/src/foundation/handle_table.h | 150 +++ .../cxx_impl/src/foundation/id_allocator.cpp | 58 + .../cxx_impl/src/foundation/id_allocator.h | 280 +++++ .../cxx_impl/src/foundation/keyboard.cpp | 39 + .../cxx_impl/src/foundation/keyboard.h | 296 +++++ .../src/foundation/native_object_provider.h | 78 ++ .../cxx_impl/src/foundation/object_registry.h | 126 ++ packages/cnativeapi/cxx_impl/src/image.h | 236 ++++ .../cxx_impl/src/keyboard_monitor.h | 36 + .../cnativeapi/cxx_impl/src/launch_at_login.h | 186 +++ packages/cnativeapi/cxx_impl/src/menu.cpp | 3 + packages/cnativeapi/cxx_impl/src/menu.h | 746 ++++++++++++ .../cnativeapi/cxx_impl/src/message_dialog.h | 136 +++ packages/cnativeapi/cxx_impl/src/placement.h | 95 ++ .../android/accessibility_manager_android.cpp | 15 + .../platform/android/application_android.cpp | 68 ++ .../platform/android/dispatcher_android.cpp | 45 + .../src/platform/android/display_android.cpp | 63 + .../android/display_manager_android.cpp | 21 + .../src/platform/android/image_android.cpp | 51 + .../android/keyboard_monitor_android.cpp | 34 + .../android/launch_at_login_android.cpp | 104 ++ .../src/platform/android/menu_android.cpp | 87 ++ .../platform/android/menu_item_android.cpp | 98 ++ .../android/message_dialog_android.cpp | 86 ++ .../platform/android/preferences_android.cpp | 103 ++ .../android/secure_storage_android.cpp | 107 ++ .../android/shortcut_manager_android.cpp | 27 + .../platform/android/tray_icon_android.cpp | 97 ++ .../platform/android/tray_manager_android.cpp | 39 + .../platform/android/url_opener_android.cpp | 26 + .../src/platform/android/window_android.cpp | 382 ++++++ .../android/window_manager_android.cpp | 157 +++ .../platform/ios/accessibility_manager_ios.mm | 18 + .../src/platform/ios/application_ios.mm | 70 ++ .../src/platform/ios/dispatcher_ios.mm | 45 + .../cxx_impl/src/platform/ios/display_ios.mm | 107 ++ .../src/platform/ios/display_manager_ios.mm | 43 + .../cxx_impl/src/platform/ios/image_ios.mm | 123 ++ .../src/platform/ios/keyboard_monitor_ios.mm | 28 + .../src/platform/ios/launch_at_login_ios.mm | 107 ++ .../cxx_impl/src/platform/ios/menu_ios.mm | 210 ++++ .../src/platform/ios/message_dialog_ios.mm | 94 ++ .../src/platform/ios/preferences_ios.mm | 104 ++ .../src/platform/ios/secure_storage_ios.mm | 108 ++ .../src/platform/ios/shortcut_manager_ios.mm | 27 + .../src/platform/ios/tray_icon_ios.mm | 93 ++ .../src/platform/ios/tray_manager_ios.mm | 39 + .../src/platform/ios/url_opener_ios.mm | 140 +++ .../cxx_impl/src/platform/ios/window_ios.mm | 425 +++++++ .../src/platform/ios/window_manager_ios.mm | 88 ++ .../linux/accessibility_manager_linux.cpp | 62 + .../src/platform/linux/application_linux.cpp | 246 ++++ .../src/platform/linux/dispatcher_linux.cpp | 61 + .../src/platform/linux/display_linux.cpp | 101 ++ .../platform/linux/display_manager_linux.cpp | 59 + .../src/platform/linux/image_linux.cpp | 272 +++++ .../platform/linux/keyboard_monitor_linux.cpp | 221 ++++ .../platform/linux/launch_at_login_linux.cpp | 377 ++++++ .../src/platform/linux/menu_linux.cpp | 893 ++++++++++++++ .../platform/linux/message_dialog_linux.cpp | 197 +++ .../src/platform/linux/preferences_linux.cpp | 193 +++ .../platform/linux/secure_storage_linux.cpp | 107 ++ .../platform/linux/shortcut_manager_linux.cpp | 397 ++++++ .../src/platform/linux/tray_icon_linux.cpp | 914 ++++++++++++++ .../src/platform/linux/tray_manager_linux.cpp | 58 + .../src/platform/linux/url_opener_linux.cpp | 45 + .../src/platform/linux/window_linux.cpp | 647 ++++++++++ .../platform/linux/window_manager_linux.cpp | 490 ++++++++ .../macos/accessibility_manager_macos.mm | 16 + .../src/platform/macos/application_macos.mm | 261 ++++ .../platform/macos/coordinate_utils_macos.h | 51 + .../src/platform/macos/dispatcher_macos.mm | 73 ++ .../src/platform/macos/display_macos.mm | 160 +++ .../platform/macos/display_manager_macos.mm | 61 + .../src/platform/macos/image_macos.mm | 210 ++++ .../platform/macos/keyboard_monitor_macos.mm | 133 ++ .../platform/macos/launch_at_login_macos.mm | 322 +++++ .../cxx_impl/src/platform/macos/menu_macos.mm | 799 ++++++++++++ .../platform/macos/message_dialog_macos.mm | 171 +++ .../src/platform/macos/preferences_macos.mm | 167 +++ .../platform/macos/secure_storage_macos.mm | 108 ++ .../platform/macos/shortcut_manager_macos.mm | 346 ++++++ .../src/platform/macos/tray_icon_macos.mm | 428 +++++++ .../src/platform/macos/tray_manager_macos.mm | 79 ++ .../src/platform/macos/url_opener_macos.mm | 56 + .../src/platform/macos/window_macos.mm | 544 +++++++++ .../platform/macos/window_manager_macos.mm | 467 ++++++++ .../ohos/accessibility_manager_ohos.cpp | 23 + .../src/platform/ohos/application_ohos.cpp | 71 ++ .../src/platform/ohos/dispatcher_ohos.cpp | 34 + .../platform/ohos/display_manager_ohos.cpp | 25 + .../src/platform/ohos/display_ohos.cpp | 79 ++ .../cxx_impl/src/platform/ohos/image_ohos.cpp | 69 ++ .../platform/ohos/keyboard_monitor_ohos.cpp | 46 + .../platform/ohos/launch_at_login_ohos.cpp | 105 ++ .../src/platform/ohos/menu_item_ohos.cpp | 115 ++ .../cxx_impl/src/platform/ohos/menu_ohos.cpp | 103 ++ .../src/platform/ohos/message_dialog_ohos.cpp | 85 ++ .../src/platform/ohos/preferences_ohos.cpp | 103 ++ .../src/platform/ohos/secure_storage_ohos.cpp | 107 ++ .../platform/ohos/shortcut_manager_ohos.cpp | 27 + .../src/platform/ohos/tray_icon_ohos.cpp | 113 ++ .../src/platform/ohos/tray_manager_ohos.cpp | 52 + .../src/platform/ohos/url_opener_ohos.cpp | 26 + .../src/platform/ohos/window_manager_ohos.cpp | 153 +++ .../src/platform/ohos/window_ohos.cpp | 366 ++++++ .../windows/accessibility_manager_windows.cpp | 26 + .../platform/windows/application_windows.cpp | 257 ++++ .../platform/windows/dispatcher_windows.cpp | 120 ++ .../windows/display_manager_windows.cpp | 62 + .../src/platform/windows/display_windows.cpp | 120 ++ .../platform/windows/dpi_utils_windows.cpp | 69 ++ .../src/platform/windows/dpi_utils_windows.h | 12 + .../src/platform/windows/image_windows.cpp | 430 +++++++ .../windows/keyboard_monitor_windows.cpp | 125 ++ .../windows/launch_at_login_windows.cpp | 287 +++++ .../src/platform/windows/menu_windows.cpp | 827 +++++++++++++ .../windows/message_dialog_windows.cpp | 104 ++ .../platform/windows/preferences_windows.cpp | 199 +++ .../windows/secure_storage_windows.cpp | 107 ++ .../windows/shortcut_manager_windows.cpp | 383 ++++++ .../platform/windows/string_utils_windows.h | 44 + .../platform/windows/tray_icon_windows.cpp | 403 +++++++ .../platform/windows/tray_manager_windows.cpp | 55 + .../platform/windows/url_opener_windows.cpp | 42 + .../windows/window_manager_windows.cpp | 561 +++++++++ .../windows/window_message_dispatcher.cpp | 239 ++++ .../windows/window_message_dispatcher.h | 166 +++ .../src/platform/windows/window_windows.cpp | 983 +++++++++++++++ .../cxx_impl/src/positioning_strategy.cpp | 46 + .../cxx_impl/src/positioning_strategy.h | 180 +++ .../cnativeapi/cxx_impl/src/preferences.cpp | 14 + .../cnativeapi/cxx_impl/src/preferences.h | 68 ++ .../cxx_impl/src/secure_storage.cpp | 14 + .../cnativeapi/cxx_impl/src/secure_storage.h | 74 ++ packages/cnativeapi/cxx_impl/src/shortcut.cpp | 65 + packages/cnativeapi/cxx_impl/src/shortcut.h | 553 +++++++++ .../cxx_impl/src/shortcut_manager.cpp | 254 ++++ .../cxx_impl/src/shortcut_manager.h | 438 +++++++ packages/cnativeapi/cxx_impl/src/storage.h | 83 ++ packages/cnativeapi/cxx_impl/src/tray_icon.h | 502 ++++++++ .../cnativeapi/cxx_impl/src/tray_manager.cpp | 10 + .../cnativeapi/cxx_impl/src/tray_manager.h | 128 ++ .../cnativeapi/cxx_impl/src/url_opener.cpp | 103 ++ packages/cnativeapi/cxx_impl/src/url_opener.h | 52 + packages/cnativeapi/cxx_impl/src/window.h | 941 +++++++++++++++ .../cxx_impl/src/window_manager.cpp | 10 + .../cnativeapi/cxx_impl/src/window_manager.h | 210 ++++ .../cxx_impl/src/window_registry.cpp | 44 + .../cnativeapi/cxx_impl/src/window_registry.h | 31 + .../cnativeapi/cxx_impl/tests/CMakeLists.txt | 31 + .../cxx_impl/tests/event_emitter_test.cpp | 526 ++++++++ .../cxx_impl/tests/handle_table_test.cpp | 333 +++++ .../cxx_impl/tests/id_allocator_test.cpp | 194 +++ .../tests/shortcut_accelerator_test.cpp | 145 +++ .../cxx_impl/tests/url_opener_test.cpp | 85 ++ .../tests/window_manager_hook_test.cpp | 182 +++ 287 files changed, 46173 insertions(+), 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 packages/cnativeapi/cxx_impl create mode 100644 packages/cnativeapi/cxx_impl/.clang-format create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/c-api-bindings.mdc create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/event-system.mdc create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/native-object-provider.mdc create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/pimpl-pattern.mdc create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/platform-implementation.mdc create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/project-architecture.mdc create mode 100644 packages/cnativeapi/cxx_impl/.cursor/rules/singleton-managers.mdc create mode 100644 packages/cnativeapi/cxx_impl/.github/workflows/build.yml create mode 100644 packages/cnativeapi/cxx_impl/.gitignore create mode 100644 packages/cnativeapi/cxx_impl/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/LICENSE create mode 100644 packages/cnativeapi/cxx_impl/README.md create mode 100644 packages/cnativeapi/cxx_impl/examples/application_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/application_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/application_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/application_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/display_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/display_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/display_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/display_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/id_allocator_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/id_allocator_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/keyboard_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/keyboard_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/launch_at_login_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/launch_at_login_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/menu_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/menu_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/menu_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/menu_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/message_dialog_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/message_dialog_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/shortcut_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/shortcut_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/shortcut_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/shortcut_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/storage_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/storage_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/storage_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/storage_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/tray_icon_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/tray_icon_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/examples/url_opener_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/url_opener_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/window_c_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/window_c_example/main.c create mode 100644 packages/cnativeapi/cxx_impl/examples/window_example/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/examples/window_example/main.cpp create mode 100644 packages/cnativeapi/cxx_impl/include/nativeapi.h create mode 100644 packages/cnativeapi/cxx_impl/src/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/src/accessibility_manager.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/accessibility_manager.h create mode 100644 packages/cnativeapi/cxx_impl/src/application.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/application.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/application_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/application_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/color_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/color_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/common_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/dialog_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/dialog_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/display_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/display_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/display_manager_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/display_manager_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/geometry_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/geometry_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/image_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/image_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/keyboard_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/keyboard_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/menu_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/menu_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/placement_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/placement_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/preferences_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/preferences_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/shortcut_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/shortcut_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/string_utils_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/string_utils_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/url_opener_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/url_opener_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/window_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/window_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/capi/window_manager_c.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/capi/window_manager_c.h create mode 100644 packages/cnativeapi/cxx_impl/src/dialog.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/dialog.h create mode 100644 packages/cnativeapi/cxx_impl/src/display.h create mode 100644 packages/cnativeapi/cxx_impl/src/display_manager.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/display_manager.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/color.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/color.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/dispatcher.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/dispatcher.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/dispatcher_common.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/dispatcher_platform.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/event.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/event_emitter.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/geometry.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/handle_table.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/handle_table.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/id_allocator.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/id_allocator.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/keyboard.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/keyboard.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/native_object_provider.h create mode 100644 packages/cnativeapi/cxx_impl/src/foundation/object_registry.h create mode 100644 packages/cnativeapi/cxx_impl/src/image.h create mode 100644 packages/cnativeapi/cxx_impl/src/keyboard_monitor.h create mode 100644 packages/cnativeapi/cxx_impl/src/launch_at_login.h create mode 100644 packages/cnativeapi/cxx_impl/src/menu.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/menu.h create mode 100644 packages/cnativeapi/cxx_impl/src/message_dialog.h create mode 100644 packages/cnativeapi/cxx_impl/src/placement.h create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/accessibility_manager_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/application_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/dispatcher_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/display_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/display_manager_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/image_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/keyboard_monitor_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/launch_at_login_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/menu_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/menu_item_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/message_dialog_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/preferences_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/secure_storage_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/shortcut_manager_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/tray_icon_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/tray_manager_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/url_opener_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/window_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/android/window_manager_android.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/accessibility_manager_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/application_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/dispatcher_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/display_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/display_manager_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/image_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/keyboard_monitor_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/launch_at_login_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/menu_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/message_dialog_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/preferences_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/secure_storage_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/shortcut_manager_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/tray_icon_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/tray_manager_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/url_opener_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/window_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/accessibility_manager_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/application_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/dispatcher_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/display_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/display_manager_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/image_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/keyboard_monitor_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/launch_at_login_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/menu_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/message_dialog_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/preferences_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/secure_storage_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/shortcut_manager_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/tray_icon_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/tray_manager_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/url_opener_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/window_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/linux/window_manager_linux.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/accessibility_manager_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/application_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/coordinate_utils_macos.h create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/dispatcher_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/display_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/display_manager_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/image_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/keyboard_monitor_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/launch_at_login_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/menu_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/message_dialog_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/preferences_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/secure_storage_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/shortcut_manager_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/tray_icon_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/tray_manager_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/url_opener_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/macos/window_manager_macos.mm create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/accessibility_manager_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/application_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/dispatcher_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/display_manager_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/display_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/image_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/keyboard_monitor_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/launch_at_login_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/menu_item_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/menu_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/message_dialog_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/preferences_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/secure_storage_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/shortcut_manager_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/tray_icon_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/tray_manager_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/url_opener_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/window_manager_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/ohos/window_ohos.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/accessibility_manager_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/application_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/dispatcher_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/display_manager_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/display_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.h create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/image_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/keyboard_monitor_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/launch_at_login_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/menu_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/message_dialog_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/preferences_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/secure_storage_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/shortcut_manager_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/string_utils_windows.h create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/tray_icon_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/tray_manager_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/url_opener_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/window_manager_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.h create mode 100644 packages/cnativeapi/cxx_impl/src/platform/windows/window_windows.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/positioning_strategy.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/positioning_strategy.h create mode 100644 packages/cnativeapi/cxx_impl/src/preferences.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/preferences.h create mode 100644 packages/cnativeapi/cxx_impl/src/secure_storage.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/secure_storage.h create mode 100644 packages/cnativeapi/cxx_impl/src/shortcut.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/shortcut.h create mode 100644 packages/cnativeapi/cxx_impl/src/shortcut_manager.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/shortcut_manager.h create mode 100644 packages/cnativeapi/cxx_impl/src/storage.h create mode 100644 packages/cnativeapi/cxx_impl/src/tray_icon.h create mode 100644 packages/cnativeapi/cxx_impl/src/tray_manager.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/tray_manager.h create mode 100644 packages/cnativeapi/cxx_impl/src/url_opener.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/url_opener.h create mode 100644 packages/cnativeapi/cxx_impl/src/window.h create mode 100644 packages/cnativeapi/cxx_impl/src/window_manager.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/window_manager.h create mode 100644 packages/cnativeapi/cxx_impl/src/window_registry.cpp create mode 100644 packages/cnativeapi/cxx_impl/src/window_registry.h create mode 100644 packages/cnativeapi/cxx_impl/tests/CMakeLists.txt create mode 100644 packages/cnativeapi/cxx_impl/tests/event_emitter_test.cpp create mode 100644 packages/cnativeapi/cxx_impl/tests/handle_table_test.cpp create mode 100644 packages/cnativeapi/cxx_impl/tests/id_allocator_test.cpp create mode 100644 packages/cnativeapi/cxx_impl/tests/shortcut_accelerator_test.cpp create mode 100644 packages/cnativeapi/cxx_impl/tests/url_opener_test.cpp create mode 100644 packages/cnativeapi/cxx_impl/tests/window_manager_hook_test.cpp diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 159e9bd..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "packages/cnativeapi/cxx_impl"] - path = packages/cnativeapi/cxx_impl - url = https://github.com/libnativeapi/nativeapi.git diff --git a/packages/cnativeapi/cxx_impl b/packages/cnativeapi/cxx_impl deleted file mode 160000 index fb69854..0000000 --- a/packages/cnativeapi/cxx_impl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fb698541687ba352e2ad36666025b8992086dca1 diff --git a/packages/cnativeapi/cxx_impl/.clang-format b/packages/cnativeapi/cxx_impl/.clang-format new file mode 100644 index 0000000..a63f13f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.clang-format @@ -0,0 +1,10 @@ +# Defines the Chromium style for automatic reformatting. +# http://clang.llvm.org/docs/ClangFormatStyleOptions.html +BasedOnStyle: Chromium +# This defaults to 'Auto'. Explicitly set it for a while, so that +# 'vector >' in existing files gets formatted to +# 'vector>'. ('Auto' means that clang-format will only use +# 'int>>' if the file already contains at least one such instance.) +Standard: Cpp11 +SortIncludes: true +ColumnLimit: 100 diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/c-api-bindings.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/c-api-bindings.mdc new file mode 100644 index 0000000..efe35e0 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/c-api-bindings.mdc @@ -0,0 +1,555 @@ +--- +globs: capi/*.c,capi/*.h +description: Creating C API bindings for C++ APIs to enable FFI from other languages +--- + +# C API Bindings Rules + +The nativeapi library provides C-compatible bindings for all C++ APIs to enable Foreign Function Interface (FFI) from languages like Dart, Swift, Rust, Python, etc. All C API code lives in [src/capi/](mdc:src/capi). + +## Why C API Bindings? + +1. **Language Interoperability** - C is the universal FFI standard +2. **Stable ABI** - C has predictable memory layout and calling conventions +3. **No Name Mangling** - C functions have simple, predictable names +4. **Simplicity** - C types map directly to FFI types in most languages + +## File Organization + +For each C++ API, create corresponding C binding files: + +| C++ API | C API Header | C API Implementation | +|---------|-------------|---------------------| +| [window.h](mdc:src/window.h) | [window_c.h](mdc:src/capi/window_c.h) | [window_c.cpp](mdc:src/capi/window_c.cpp) | +| [window_manager.h](mdc:src/window_manager.h) | [window_manager_c.h](mdc:src/capi/window_manager_c.h) | [window_manager_c.cpp](mdc:src/capi/window_manager_c.cpp) | +| [menu.h](mdc:src/menu.h) | [menu_c.h](mdc:src/capi/menu_c.h) | [menu_c.cpp](mdc:src/capi/menu_c.cpp) | + +## C API Header Pattern + +### Basic Structure ([window_c.h](mdc:src/capi/window_c.h)) + +```c +#pragma once + +#include +#include + +// Export macro for DLL support +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#include "geometry_c.h" // Include C versions of dependencies + +// Opaque handle type +typedef void* native_window_t; +typedef long native_window_id_t; + +// C struct for options (plain C types only) +typedef struct { + const char* title; + native_size_t size; + native_size_t minimum_size; + native_size_t maximum_size; + bool centered; +} native_window_options_t; + +// C functions matching C++ API +FFI_PLUGIN_EXPORT +native_window_t native_window_create(const native_window_options_t* options); + +FFI_PLUGIN_EXPORT +void native_window_destroy(native_window_t window); + +FFI_PLUGIN_EXPORT +native_window_id_t native_window_get_id(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_show(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_hide(native_window_t window); + +FFI_PLUGIN_EXPORT +bool native_window_is_visible(native_window_t window); + +// Memory management helpers +FFI_PLUGIN_EXPORT +void native_window_free(native_window_t window); + +#ifdef __cplusplus +} +#endif +``` + +### Key Elements + +1. **Export Macro** - `FFI_PLUGIN_EXPORT` for DLL/shared library support +2. **Extern "C" Block** - Prevents C++ name mangling +3. **Opaque Pointers** - `typedef void* native_xxx_t` for object handles +4. **Plain C Types** - Only `bool`, `int`, `float`, `double`, `char*`, structs +5. **Naming Convention** - `native__` pattern +6. **Documentation** - Comment each function for FFI consumers + +## C API Implementation Pattern + +### Basic Structure ([window_c.cpp](mdc:src/capi/window_c.cpp)) + +```cpp +#include "window_c.h" +#include +#include "../window.h" +#include "../window_manager.h" +#include "string_utils_c.h" + +using namespace nativeapi; + +// Convert C options to C++ options +static WindowOptions ConvertToWindowOptions(const native_window_options_t* options) { + WindowOptions cpp_options; + + if (options->title) { + cpp_options.title = std::string(options->title); + } + + cpp_options.size.width = options->size.width; + cpp_options.size.height = options->size.height; + cpp_options.minimum_size.width = options->minimum_size.width; + cpp_options.minimum_size.height = options->minimum_size.height; + cpp_options.maximum_size.width = options->maximum_size.width; + cpp_options.maximum_size.height = options->maximum_size.height; + cpp_options.centered = options->centered; + + return cpp_options; +} + +// Convert C++ window to C handle +static native_window_t WindowToHandle(std::shared_ptr window) { + return window ? static_cast(window.get()) : nullptr; +} + +// Convert C handle to C++ window +static std::shared_ptr HandleToWindow(native_window_t handle) { + if (!handle) return nullptr; + + // Get from manager's internal registry + Window* raw_ptr = static_cast(handle); + return WindowManager::GetInstance().Get(raw_ptr->GetId()); +} + +// Implement C functions +native_window_t native_window_create(const native_window_options_t* options) { + if (!options) return nullptr; + + try { + auto cpp_options = ConvertToWindowOptions(options); + auto window = WindowManager::GetInstance().Create(cpp_options); + return WindowToHandle(window); + } catch (...) { + return nullptr; + } +} + +void native_window_destroy(native_window_t window) { + try { + auto cpp_window = HandleToWindow(window); + if (cpp_window) { + WindowManager::GetInstance().Destroy(cpp_window->GetId()); + } + } catch (...) { + // Ignore exceptions + } +} + +native_window_id_t native_window_get_id(native_window_t window) { + try { + auto cpp_window = HandleToWindow(window); + return cpp_window ? cpp_window->GetId() : 0; + } catch (...) { + return 0; + } +} + +void native_window_show(native_window_t window) { + try { + auto cpp_window = HandleToWindow(window); + if (cpp_window) { + cpp_window->Show(); + } + } catch (...) { + // Ignore exceptions + } +} + +bool native_window_is_visible(native_window_t window) { + try { + auto cpp_window = HandleToWindow(window); + return cpp_window ? cpp_window->IsVisible() : false; + } catch (...) { + return false; + } +} + +void native_window_free(native_window_t window) { + // Handle is managed by WindowManager, nothing to free + // This exists for API consistency +} +``` + +### Key Implementation Patterns + +1. **Exception Safety** - Wrap all calls in try-catch +2. **Null Checks** - Always validate handles before use +3. **Conversion Helpers** - Static functions for C ↔ C++ conversion +4. **Manager Integration** - Use managers to resolve handles to shared_ptr +5. **Memory Safety** - C handles are weak references, manager owns objects + +## Type Mapping + +### Primitive Types + +| C++ Type | C Type | +|----------|--------| +| `bool` | `bool` | +| `int`, `long` | `int`, `long` | +| `float`, `double` | `float`, `double` | +| `std::string` | `const char*` | + +### Complex Types + +| C++ Type | C Type | Notes | +|----------|--------|-------| +| `std::shared_ptr` | `native_window_t` (void*) | Opaque handle | +| `std::vector` | `native_window_list_t` | Custom struct with array | +| `WindowOptions` | `native_window_options_t` | Plain C struct | +| `enum class MenuItemType` | `native_menu_item_type_t` (int) | Plain enum | +| `std::optional` | `const char*` | Use `nullptr` for none | + +### Geometry Types ([geometry_c.h](mdc:src/capi/geometry_c.h)) + +```c +typedef struct { + double x; + double y; +} native_point_t; + +typedef struct { + double width; + double height; +} native_size_t; + +typedef struct { + double x; + double y; + double width; + double height; +} native_rectangle_t; +``` + +## Event Handling Pattern + +### C Callback Functions ([window_manager_c.h](mdc:src/capi/window_manager_c.h)) + +```c +// Event type enum +typedef enum { + NATIVE_WINDOW_EVENT_CREATED = 0, + NATIVE_WINDOW_EVENT_CLOSED = 1, + NATIVE_WINDOW_EVENT_FOCUSED = 2, + NATIVE_WINDOW_EVENT_MOVED = 3, +} native_window_event_type_t; + +// Event data structure +typedef struct { + native_window_event_type_t type; + native_window_id_t window_id; + union { + struct { + native_point_t position; + } moved; + struct { + native_size_t size; + } resized; + } data; +} native_window_event_t; + +// Callback function type +typedef void (*native_window_event_callback_t)( + const native_window_event_t* event, + void* user_data +); + +// Register callback +FFI_PLUGIN_EXPORT +int native_window_manager_register_event_callback( + native_window_event_callback_t callback, + void* user_data +); + +// Unregister callback +FFI_PLUGIN_EXPORT +bool native_window_manager_unregister_event_callback(int registration_id); +``` + +### C Callback Implementation ([window_manager_c.cpp](mdc:src/capi/window_manager_c.cpp)) + +```cpp +// Global state for callbacks +struct EventCallbackInfo { + native_window_event_callback_t callback; + void* user_data; + int id; +}; + +static std::mutex g_callback_mutex; +static std::unordered_map g_event_callbacks; +static int g_next_callback_id = 1; + +// Bridge C++ events to C callbacks +class CEventListener { +public: + CEventListener() { + auto& manager = WindowManager::GetInstance(); + + manager.AddListener( + [this](const WindowCreatedEvent& e) { + native_window_event_t event; + event.type = NATIVE_WINDOW_EVENT_CREATED; + event.window_id = e.GetWindowId(); + DispatchEvent(event); + } + ); + + // ... register for other events + } + +private: + void DispatchEvent(const native_window_event_t& event) { + std::lock_guard lock(g_callback_mutex); + + for (const auto& [id, info] : g_event_callbacks) { + try { + info.callback(&event, info.user_data); + } catch (...) { + // Ignore exceptions from callbacks + } + } + } +}; + +// Initialize event listener on first callback registration +static CEventListener* g_event_listener = nullptr; + +int native_window_manager_register_event_callback( + native_window_event_callback_t callback, + void* user_data +) { + if (!callback) return -1; + + std::lock_guard lock(g_callback_mutex); + + // Initialize listener if needed + if (!g_event_listener) { + g_event_listener = new CEventListener(); + } + + int id = g_next_callback_id++; + g_event_callbacks[id] = {callback, user_data, id}; + + return id; +} + +bool native_window_manager_unregister_event_callback(int registration_id) { + std::lock_guard lock(g_callback_mutex); + return g_event_callbacks.erase(registration_id) > 0; +} +``` + +## String Handling + +### Utility Functions ([string_utils_c.h](mdc:src/capi/string_utils_c.h)) + +```c +// Allocate C string from C++ string +FFI_PLUGIN_EXPORT +char* native_string_allocate(const char* str); + +// Free C string +FFI_PLUGIN_EXPORT +void native_string_free(char* str); +``` + +### Implementation ([string_utils_c.cpp](mdc:src/capi/string_utils_c.cpp)) + +```cpp +char* native_string_allocate(const char* str) { + if (!str) return nullptr; + + size_t len = strlen(str); + char* result = static_cast(malloc(len + 1)); + + if (result) { + strcpy(result, str); + } + + return result; +} + +void native_string_free(char* str) { + if (str) { + free(str); + } +} +``` + +### String Return Pattern + +```cpp +// Method returning string +FFI_PLUGIN_EXPORT +char* native_window_get_title(native_window_t window) { + try { + auto cpp_window = HandleToWindow(window); + if (!cpp_window) return nullptr; + + std::string title = cpp_window->GetTitle(); + return native_string_allocate(title.c_str()); + } catch (...) { + return nullptr; + } +} + +// Caller must free +char* title = native_window_get_title(window); +if (title) { + // Use title + printf("Title: %s\n", title); + + // Free when done + native_string_free(title); +} +``` + +## Collection Handling + +### List Pattern ([window_manager_c.h](mdc:src/capi/window_manager_c.h)) + +```c +typedef struct { + native_window_t* windows; + size_t count; +} native_window_list_t; + +FFI_PLUGIN_EXPORT +native_window_list_t native_window_manager_get_all(void); + +FFI_PLUGIN_EXPORT +void native_window_list_free(native_window_list_t list); +``` + +### Implementation + +```cpp +native_window_list_t native_window_manager_get_all(void) { + native_window_list_t result = {nullptr, 0}; + + try { + auto windows = WindowManager::GetInstance().GetAll(); + + if (windows.empty()) { + return result; + } + + result.count = windows.size(); + result.windows = static_cast( + malloc(sizeof(native_window_t) * result.count) + ); + + if (result.windows) { + for (size_t i = 0; i < windows.size(); ++i) { + result.windows[i] = WindowToHandle(windows[i]); + } + } + } catch (...) { + // Return empty list + } + + return result; +} + +void native_window_list_free(native_window_list_t list) { + if (list.windows) { + free(list.windows); + } +} +``` + +## Best Practices + +1. **Always wrap in try-catch** - C callers can't handle C++ exceptions +2. **Return error indicators** - `nullptr`, `false`, `-1`, etc. on error +3. **Validate all inputs** - Check for null pointers, invalid handles +4. **Use opaque handles** - Never expose C++ objects directly +5. **Provide free functions** - Match every allocate with a free +6. **Document memory ownership** - Who allocates? Who frees? +7. **Thread safety** - Assume C callers are multi-threaded +8. **Keep it simple** - C API should be straightforward to use + +## Testing C API + +Create C example programs ([examples/window_c_example/main.c](mdc:examples/window_c_example/main.c)): + +```c +#include "nativeapi.h" +#include + +void on_window_event(const native_window_event_t* event, void* user_data) { + printf("Window event: %d\n", event->type); +} + +int main() { + // Create window + native_window_options_t options = { + .title = "C API Example", + .size = {800, 600}, + .minimum_size = {400, 300}, + .maximum_size = {0, 0}, + .centered = true + }; + + native_window_t window = native_window_manager_create(&options); + if (!window) { + printf("Failed to create window\n"); + return 1; + } + + // Register event callback + int callback_id = native_window_manager_register_event_callback( + on_window_event, + NULL + ); + + // Show window + native_window_show(window); + + // ... run event loop ... + + // Cleanup + native_window_manager_unregister_event_callback(callback_id); + native_window_destroy(window); + + return 0; +} +``` + +## Related Topics + +- See [Project Architecture Rules](mdc:.cursor/rules/project-architecture.mdc) for overall structure +- See [Singleton Managers Rules](mdc:.cursor/rules/singleton-managers.mdc) for manager pattern +- See [Event System Rules](mdc:.cursor/rules/event-system.mdc) for event handling diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/event-system.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/event-system.mdc new file mode 100644 index 0000000..ecc2dad --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/event-system.mdc @@ -0,0 +1,337 @@ +--- +alwaysApply: true +description: Generic event system architecture and usage patterns +--- + +# Generic Event System Rules + +This project uses a comprehensive, type-safe event system built on top of C++ templates and inheritance. Understanding this system is crucial for working with any event-driven components. + +## Core Architecture + +### Base Event Class + +All events inherit from [Event](mdc:src/foundation/event.h) which provides: + +- Automatic timestamp generation +- Virtual `GetTypeName()` method for debugging +- Type-safe event hierarchy + +### Event Emitter Pattern + +Classes that emit events inherit from `EventEmitter` from [event_emitter.h](mdc:src/foundation/event_emitter.h): + +- Provides compile-time type safety +- Supports both synchronous and asynchronous event emission +- Thread-safe listener management +- Automatic background thread management for async events + +## Event Types + +Events are organized into hierarchies based on their domain. Each hierarchy has a base event class that other specific events inherit from. This provides type safety and allows listeners to register for either specific events or entire categories. + +## Usage Patterns + +### Creating Event Classes + +```cpp +class MyCustomEvent : public Event { +public: + MyCustomEvent(const std::string& data) : data_(data) {} + + const std::string& GetData() const { return data_; } + std::string GetTypeName() const override { return "MyCustomEvent"; } + +private: + std::string data_; +}; +``` + +### Creating Event Emitters + +```cpp +class MyClass : public EventEmitter { +public: + void DoSomething() { + // Synchronous emission + Emit("some data"); + + // Asynchronous emission + EmitAsync("async data"); + } +}; +``` + +### Adding Event Listeners + +```cpp +// Using lambda functions +auto listener_id = emitter.AddListener( + [](const MyCustomEvent& event) { + std::cout << "Received: " << event.GetData() << std::endl; + } +); + +// Using custom listener class +class MyListener : public EventListener { +public: + void OnEvent(const MyCustomEvent& event) override { + // Handle event + } +}; + +MyListener listener; +auto listener_id = emitter.AddListener(&listener); +``` + +### Removing Listeners + +```cpp +// Remove by ID +emitter.RemoveListener(listener_id); + +// Remove all listeners for specific event type +emitter.RemoveAllListeners(); + +// Remove all listeners +emitter.RemoveAllListeners(); +``` + +## Best Practices + +1. **Always inherit from appropriate base event class** - Don't inherit directly from `Event` unless creating a new event hierarchy +2. **Use specific event types** - Prefer `EventListener` over `EventListener` for type safety +3. **Implement GetTypeName()** - Always override this method for debugging purposes +4. **Use const references** - Event handlers should accept `const EventType&` parameters +5. **Manage listener lifetimes** - Ensure listener objects remain valid while registered +6. **Prefer async emission for heavy operations** - Use `EmitAsync` for events that might trigger expensive operations +7. **Use RAII for listener management** - Store listener IDs and remove them in destructors + +## Thread Safety + +- Event emission is thread-safe +- Listener registration/removal is thread-safe +- Async event processing uses a dedicated background thread +- Event handlers may be called from different threads depending on emission method + +## Common Patterns + +### Singleton Event Emitters + +Many managers (WindowManager, DisplayManager) are singletons that emit events: + +```cpp +auto& manager = WindowManager::GetInstance(); +manager.AddListener([](const WindowCreatedEvent& event) { + // Handle window creation +}); +``` + +### Event Forwarding + +Platform-specific implementations often forward system events to the generic event system: + +```cpp +void PlatformWindow::OnSystemEvent(const SystemEvent& sys_event) { + // Convert to generic event and emit + WindowEvent generic_event(sys_event.GetWindowId()); + emitter_.Emit(generic_event); +} +``` + +### Event Filtering + +Listeners can filter events by checking specific types: + +```cpp +emitter.AddListener([](const WindowEvent& event) { + if (auto moved_event = dynamic_cast(&event)) { + // Handle only window moved events + } +}); +``` + +### Event Listening Lifecycle Management + +Managers can efficiently manage platform-specific event monitoring by overriding `StartEventListening()` and `StopEventListening()` hooks. These hooks are automatically called when the first listener is added and when the last listener is removed, respectively. + +#### Purpose + +This pattern allows managers to: +- **Lazy initialization** - Only start platform event monitoring when needed +- **Resource efficiency** - Stop monitoring when no listeners exist +- **Automatic management** - No manual tracking of listener count required + +#### Implementation Pattern + +```cpp +// tray_icon.h +class TrayIcon : public EventEmitter, public NativeObjectProvider { +public: + TrayIcon(); + virtual ~TrayIcon(); + + // ... public API ... + +protected: + // Override these to control platform event monitoring + void StartEventListening() override; + void StopEventListening() override; + +private: + class Impl; + std::unique_ptr pimpl_; +}; + +// tray_icon.cpp +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) { + // DON'T call SetupEventMonitoring() here anymore! + // It will be called automatically when first listener is added +} + +void TrayIcon::StartEventListening() { + // Called automatically when first listener is added + pimpl_->SetupEventMonitoring(); +} + +void TrayIcon::StopEventListening() { + // Called automatically when last listener is removed + pimpl_->CleanupEventMonitoring(); +} +``` + +#### Platform Implementation Example + +```objc +// platform/macos/tray_icon_macos.mm +class TrayIcon::Impl { +public: + Impl(NSStatusItem* status_item) + : ns_status_item_(status_item), + ns_status_bar_button_target_(nil), + click_handler_setup_(false) {} + + void SetupEventMonitoring() { + if (click_handler_setup_) { + return; // Already monitoring + } + + if (!ns_status_item_ || !ns_status_item_.button) { + return; + } + + // Create and set up button target + ns_status_bar_button_target_ = [[NSStatusBarButtonTarget alloc] init]; + + // Set up event handlers + [ns_status_item_.button setTarget:ns_status_bar_button_target_]; + [ns_status_item_.button setAction:@selector(handleStatusItemEvent:)]; + + // Enable click handling + [ns_status_item_.button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp]; + + click_handler_setup_ = true; + } + + void CleanupEventMonitoring() { + if (!click_handler_setup_) { + return; // Not monitoring + } + + // Remove event handlers + if (ns_status_item_ && ns_status_item_.button) { + [ns_status_item_.button setTarget:nil]; + [ns_status_item_.button setAction:nil]; + } + + // Clean up button target + ns_status_bar_button_target_ = nil; + + click_handler_setup_ = false; + } + +private: + NSStatusItem* ns_status_item_; + NSStatusBarButtonTarget* ns_status_bar_button_target_; + bool click_handler_setup_; +}; +``` + +#### Usage Flow + +```cpp +// Application code +auto tray_icon = std::make_shared(); +tray_icon->SetIcon(icon); +tray_icon->SetTooltip("My Application"); + +// At this point, NO platform event monitoring is active +// (saves system resources) + +// Add first listener - triggers StartEventListening() +auto listener_id = tray_icon->AddListener( + [](const TrayIconClickedEvent& event) { + std::cout << "Tray icon clicked: " << event.GetTrayIconId() << std::endl; + } +); + +// Platform event monitoring is now ACTIVE + +// Add more listeners - StartEventListening() NOT called again +auto right_click_id = tray_icon->AddListener( + [](const TrayIconRightClickedEvent& event) { + std::cout << "Tray icon right clicked" << std::endl; + } +); + +// Remove one listener - StopEventListening() NOT called (still have listeners) +tray_icon->RemoveListener(right_click_id); + +// Remove last listener - triggers StopEventListening() +tray_icon->RemoveListener(listener_id); + +// Platform event monitoring is now STOPPED +// (saves system resources again) +``` + +#### Important Considerations + +1. **Mutex Held**: `StartEventListening()` and `StopEventListening()` are called while holding `listeners_mutex_`. Keep the implementation fast and avoid acquiring other locks that could cause deadlocks. + +2. **Default Implementation**: The default implementations are empty, so existing subclasses don't need to change unless they want to use this feature. + +3. **Transitional Calls**: These methods are only called on transitions (0 → 1+ listeners and 1+ → 0 listeners), not on every add/remove operation. + +4. **Destructor Cleanup**: If the emitter is destroyed while listeners exist, `StopEventListening()` is NOT called. Clean up resources in the destructor if needed. + +5. **Idempotent Operations**: Implement setup/cleanup methods to be safe to call multiple times without side effects. + +#### Migration from Old Pattern + +##### Before (Always Monitoring) + +```cpp +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) { + SetupEventMonitoring(); // Always monitoring +} + +TrayIcon::~TrayIcon() { + CleanupEventMonitoring(); +} +``` + +##### After (Lazy Monitoring) + +```cpp +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) { + // No need to call SetupEventMonitoring() +} + +void TrayIcon::StartEventListening() { + pimpl_->SetupEventMonitoring(); // Called when first listener added +} + +void TrayIcon::StopEventListening() { + pimpl_->CleanupEventMonitoring(); // Called when last listener removed +} +``` diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/native-object-provider.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/native-object-provider.mdc new file mode 100644 index 0000000..0dcf86c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/native-object-provider.mdc @@ -0,0 +1,519 @@ +--- +alwaysApply: true +description: Pattern for exposing platform-specific native handles from cross-platform wrappers +--- + +# Native Object Provider Pattern Rules + +Classes that wrap platform-specific objects inherit from [NativeObjectProvider](mdc:src/foundation/native_object_provider.h) to expose their underlying native handles. This enables advanced use cases where users need direct access to platform APIs while maintaining the cross-platform abstraction. + +## Purpose + +The NativeObjectProvider pattern serves several purposes: + +1. **Escape Hatch** - Allows access to native APIs not wrapped by the library +2. **Interop** - Enables integration with other libraries expecting native handles +3. **Advanced Features** - Supports platform-specific functionality +4. **Type Safety** - Returns `void*` for cross-platform compatibility +5. **Encapsulation** - Keeps implementation details hidden until explicitly requested + +## Base Class Structure + +### Header ([foundation/native_object_provider.h](mdc:src/foundation/native_object_provider.h)) + +```cpp +#pragma once + +namespace nativeapi { + +class NativeObjectProvider { +public: + virtual ~NativeObjectProvider() = default; + + /** + * Get the native platform-specific object. + * + * Platform-specific return types: + * - macOS: NSWindow*, NSMenu*, NSMenuItem*, NSView*, etc. + * - Windows: HWND, HMENU, etc. + * - Linux: GtkWidget*, GtkMenu*, GdkWindow*, etc. + */ + void* GetNativeObject() const { + return GetNativeObjectInternal(); + } + +protected: + /** + * Derived classes must implement this to return their native object. + */ + virtual void* GetNativeObjectInternal() const = 0; +}; + +} // namespace nativeapi +``` + +## Implementing NativeObjectProvider + +### Pattern for Classes + +All classes wrapping native objects should: + +1. Inherit from `NativeObjectProvider` +2. Implement `GetNativeObjectInternal()` protected method +3. Return platform-specific handle as `void*` + +### Example: Window Class + +#### Header ([window.h](mdc:src/window.h)) + +```cpp +#pragma once +#include "foundation/native_object_provider.h" + +namespace nativeapi { + +class Window : public NativeObjectProvider { +public: + Window(); + Window(void* native_window); + virtual ~Window(); + + // ... public API methods ... + +protected: + void* GetNativeObjectInternal() const override; + +private: + class Impl; + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi +``` + +#### Platform Implementations + +##### Windows ([platform/windows/window_windows.cpp](mdc:src/platform/windows)) + +```cpp +#include +#include "../../window.h" + +namespace nativeapi { + +class Window::Impl { +public: + HWND hwnd_; +}; + +void* Window::GetNativeObjectInternal() const { + // Cast HWND to void* + return static_cast(pimpl_->hwnd_); +} + +} // namespace nativeapi +``` + +##### macOS ([platform/macos/window_macos.mm](mdc:src/platform/macos)) + +```objc +#import +#include "../../window.h" + +namespace nativeapi { + +class Window::Impl { +public: + NSWindow* window_; +}; + +void* Window::GetNativeObjectInternal() const { + // Cast NSWindow* to void* + return static_cast(pimpl_->window_); +} + +} // namespace nativeapi +``` + +##### Linux ([platform/linux/window_linux.cpp](mdc:src/platform/linux)) + +```cpp +#include +#include "../../window.h" + +namespace nativeapi { + +class Window::Impl { +public: + GtkWidget* window_; +}; + +void* Window::GetNativeObjectInternal() const { + // Cast GtkWidget* to void* + return static_cast(pimpl_->window_); +} + +} // namespace nativeapi +``` + +## Using Native Objects + +### Cross-Platform Usage + +Users can access native objects when they need platform-specific functionality. Since platform-specific implementations are separated into `/platform/{windows|macos|linux}` directories, users should cast the native handle to the appropriate platform-specific type: + +```cpp +#include + +using namespace nativeapi; + +auto& manager = WindowManager::GetInstance(); +auto window = manager.Create(options); + +// Get native handle +void* native = window->GetNativeObject(); + +// Cast to platform-specific type +// Windows: HWND +// macOS: NSWindow* +// Linux: GtkWidget* (GtkWindow) +``` + +### Platform-Specific Usage Examples + +#### Windows Implementation +```cpp +// In Windows-specific code +HWND hwnd = static_cast(window->GetNativeObject()); +SetWindowLongPtr(hwnd, GWL_EXSTYLE, WS_EX_LAYERED); +``` + +#### macOS Implementation +```objc +// In macOS-specific code +NSWindow* nswindow = static_cast(window->GetNativeObject()); +[nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; +``` + +#### Linux Implementation +```cpp +// In Linux-specific code +GtkWidget* gtkwindow = static_cast(window->GetNativeObject()); +gtk_window_set_keep_above(GTK_WINDOW(gtkwindow), TRUE); +``` + +## Classes Using NativeObjectProvider + +The following classes inherit from NativeObjectProvider: + +| Class | Native Type (Windows) | Native Type (macOS) | Native Type (Linux) | +|-------|----------------------|---------------------|---------------------| +| [Window](mdc:src/window.h) | `HWND` | `NSWindow*` | `GtkWidget*` (GtkWindow) | +| [Display](mdc:src/display.h) | `HMONITOR` | `NSScreen*` | `GdkDisplay*` / `GdkMonitor*` | +| [Menu](mdc:src/menu.h) | `HMENU` | `NSMenu*` | `GtkWidget*` (GtkMenu) | +| [MenuItem](mdc:src/menu.h) | N/A (part of HMENU) | `NSMenuItem*` | `GtkWidget*` (GtkMenuItem) | +| [TrayIcon](mdc:src/tray_icon.h) | `HWND` (hidden window) | `NSStatusItem*` | `AppIndicator*` | + +## Example Use Cases + +### Use Case 1: Setting Custom Window Attributes + +```cpp +// User wants to make window transparent (not in cross-platform API) +auto window = manager.Create(options); +void* native = window->GetNativeObject(); + +// Platform-specific implementations would be in separate files: +// - Windows: platform/windows/window_windows.cpp +// - macOS: platform/macos/window_macos.mm +// - Linux: platform/linux/window_linux.cpp +``` + +#### Windows Implementation +```cpp +// In platform/windows/window_windows.cpp +HWND hwnd = static_cast(native); + +// Enable transparency using Win32 API +SetWindowLongPtr(hwnd, GWL_EXSTYLE, + GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED); +SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 128, LWA_ALPHA); +``` + +#### macOS Implementation +```objc +// In platform/macos/window_macos.mm +NSWindow* nswindow = static_cast(native); + +// Enable transparency using Cocoa API +[nswindow setOpaque:NO]; +[nswindow setBackgroundColor:[NSColor colorWithRed:0 green:0 blue:0 alpha:0.5]]; +``` + +#### Linux Implementation +```cpp +// In platform/linux/window_linux.cpp +GtkWidget* gtkwindow = static_cast(native); + +// Enable transparency using GTK API +gtk_widget_set_opacity(gtkwindow, 0.5); +``` + +### Use Case 2: Integrating with Third-Party Libraries + +```cpp +// Embedding a web view that expects native window handle +auto window = manager.Create(options); +void* native = window->GetNativeObject(); + +// Platform-specific implementations would be in separate files +``` + +#### Windows Implementation +```cpp +// In platform/windows/window_windows.cpp +HWND hwnd = static_cast(native); +WebView2::Create(hwnd, ...); +``` + +#### macOS Implementation +```objc +// In platform/macos/window_macos.mm +NSWindow* nswindow = static_cast(native); +WKWebView* webview = [[WKWebView alloc] initWithFrame:[nswindow contentView].bounds]; +[[nswindow contentView] addSubview:webview]; +``` + +#### Linux Implementation +```cpp +// In platform/linux/window_linux.cpp +GtkWidget* gtkwindow = static_cast(native); +GtkWidget* webview = webkit_web_view_new(); +gtk_container_add(GTK_CONTAINER(gtkwindow), webview); +``` + +### Use Case 3: Platform-Specific Menu Customization + +```cpp +auto menu = std::make_shared(); +auto item = std::make_shared("File"); +menu->AddItem(item); + +// Platform-specific implementations would be in separate files +``` + +#### macOS Implementation +```objc +// In platform/macos/menu_macos.mm +NSMenu* nsmenu = static_cast(menu->GetNativeObject()); +[nsmenu setAutoenablesItems:NO]; + +NSMenuItem* nsitem = static_cast(item->GetNativeObject()); +[nsitem setTarget:customTarget]; +[nsitem setAction:@selector(customAction:)]; +``` + +### Use Case 4: Advanced Display Configuration + +```cpp +auto& display_manager = DisplayManager::GetInstance(); +auto primary = display_manager.GetPrimary(); +void* native = primary.GetNativeObject(); + +// Platform-specific implementations would be in separate files +``` + +#### Windows Implementation +```cpp +// In platform/windows/display_windows.cpp +HMONITOR hmonitor = static_cast(native); + +MONITORINFOEX mi = {}; +mi.cbSize = sizeof(MONITORINFOEX); +GetMonitorInfo(hmonitor, &mi); + +// Access device name for advanced configuration +std::wcout << L"Device: " << mi.szDevice << std::endl; +``` + +#### macOS Implementation +```objc +// In platform/macos/display_macos.mm +NSScreen* screen = static_cast(native); + +// Access color space information +NSColorSpace* colorSpace = [screen colorSpace]; +std::cout << "Color space: " << [[colorSpace localizedName] UTF8String] << std::endl; +``` + +## Design Considerations + +### Why void* Instead of Templates? + +```cpp +// Alternative: Template approach (NOT USED) +template +class Window { + TNative GetNativeObject() const; +}; + +// Why we don't do this: +// 1. Breaks ABI compatibility +// 2. Requires platform knowledge at compile time +// 3. Complicates cross-platform code +// 4. Makes headers include platform-specific types +``` + +The `void*` approach: +- ✅ Maintains clean cross-platform API +- ✅ Allows casting in implementation files +- ✅ No platform headers in public API +- ✅ Binary compatible across platforms + +### Null Handling + +Always check for null before using native objects: + +```cpp +void* native = window->GetNativeObject(); + +if (!native) { + // Handle error - window may not be initialized + return; +} + +// Platform-specific validation would be in separate implementation files +``` + +#### Windows Implementation +```cpp +// In platform/windows/window_windows.cpp +HWND hwnd = static_cast(native); +if (!IsWindow(hwnd)) { + // Handle invalid window + return; +} +``` + +#### macOS Implementation +```objc +// In platform/macos/window_macos.mm +NSWindow* nswindow = static_cast(native); +if (!nswindow || ![nswindow isKindOfClass:[NSWindow class]]) { + // Handle invalid window + return; +} +``` + +#### Linux Implementation +```cpp +// In platform/linux/window_linux.cpp +GtkWidget* gtkwindow = static_cast(native); +if (!GTK_IS_WINDOW(gtkwindow)) { + // Handle invalid window + return; +} +``` + +### Lifetime Considerations + +The native object lifetime is managed by the C++ wrapper: + +```cpp +auto window = manager.Create(options); +void* native = window->GetNativeObject(); + +// Native handle is valid as long as window is alive +UseNativeHandle(native); // OK + +// After destroying window, native handle becomes invalid +manager.Destroy(window->GetId()); +// native is now dangling pointer - DON'T USE! +``` + +## Best Practices + +1. **Document native types** - Comment what type is returned on each platform +2. **Validate before casting** - Check for null, platform-specific validity +3. **Don't store native handles** - They may become invalid when wrapper is destroyed +4. **Separate platform implementations** - Keep platform-specific code in `/platform/{windows|macos|linux}` directories +5. **Prefer cross-platform API** - Only use native handles when absolutely necessary +6. **Thread safety** - Native APIs may have threading restrictions +7. **Keep it simple** - Minimize platform-specific code in user code + +## Documentation Example + +When documenting APIs that return native objects: + +```cpp +/** + * @brief Get the native platform-specific window handle. + * + * This method provides access to the underlying native window object + * for advanced use cases. The lifetime of the native object is managed + * by this Window instance. + * + * @return void* pointer to native window object: + * - Windows: HWND + * - macOS: NSWindow* + * - Linux: GtkWidget* (GtkWindow) + * + * @warning The native handle becomes invalid when this Window is destroyed. + * Do not store the handle long-term. Always check for null before use. + * + * @example + * ```cpp + * void* native = window->GetNativeObject(); + * + * // Platform-specific implementations would be in separate files: + * // Windows: platform/windows/window_windows.cpp + * // macOS: platform/macos/window_macos.mm + * // Linux: platform/linux/window_linux.cpp + * ``` + */ +void* GetNativeObject() const; +``` + +## Common Pitfalls + +### ❌ Don't Cast to Wrong Type + +```cpp +// Bad - wrong type for platform +// In Windows implementation file, casting to macOS type +NSWindow* window = static_cast(native); // Error! + +// Good - correct type for platform +// In platform/windows/window_windows.cpp +HWND hwnd = static_cast(native); +``` + +### ❌ Don't Mix Platform Code + +```cpp +// Bad - mixing platform APIs in same file +HWND hwnd = static_cast(window->GetNativeObject()); +NSWindow* nswindow = static_cast(window->GetNativeObject()); // Wrong! + +// Good - separate platform implementations +// Windows: platform/windows/window_windows.cpp +// macOS: platform/macos/window_macos.mm +``` + +### ❌ Don't Manage Native Lifetime Manually + +```cpp +// Bad - trying to destroy native object directly +// In platform/windows/window_windows.cpp +HWND hwnd = static_cast(window->GetNativeObject()); +DestroyWindow(hwnd); // Wrapper still thinks it owns this! + +// Good - let wrapper manage lifetime +manager.Destroy(window->GetId()); +``` + +## Related Topics + +- See [PIMPL Pattern Rules](mdc:.cursor/rules/pimpl-pattern.mdc) for implementation hiding +- See [Platform Implementation Rules](mdc:.cursor/rules/platform-implementation.mdc) for platform code +- See [Project Architecture Rules](mdc:.cursor/rules/project-architecture.mdc) for overall structure diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/pimpl-pattern.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/pimpl-pattern.mdc new file mode 100644 index 0000000..b5a3633 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/pimpl-pattern.mdc @@ -0,0 +1,434 @@ +--- +alwaysApply: true +description: PIMPL (Pointer to Implementation) pattern for platform-specific code +--- + +# PIMPL Pattern Rules + +The nativeapi library uses the PIMPL (Pointer to Implementation) idiom extensively to hide platform-specific implementation details from the public API. This provides binary compatibility, reduces compilation dependencies, and enables clean platform abstraction. + +## What is PIMPL? + +PIMPL separates a class's interface from its implementation by: +1. Declaring a private nested `Impl` class in the header +2. Storing only a pointer to the implementation +3. Implementing platform-specific logic in the source files + +## Pattern Structure + +### Header File Pattern ([window.h](mdc:src/window.h)) + +```cpp +#pragma once +#include + +namespace nativeapi { + +class Window { +public: + Window(); + Window(void* native_window); + virtual ~Window(); + + // Public interface methods + void Show(); + void Hide(); + bool IsVisible() const; + +private: + // Forward declaration only - no definition + class Impl; + + // Pointer to implementation + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi +``` + +**Key Points:** +- Forward declare `Impl` class - don't define it in header +- Use `std::unique_ptr` for automatic cleanup +- No platform-specific includes in header +- No platform-specific types in public interface + +### Platform-Specific Implementation Files + +Each platform provides its own `Impl` definition: + +#### Windows Implementation ([platform/windows/window_windows.cpp](mdc:src/platform/windows)) + +```cpp +#include // Platform includes only in .cpp +#include "../../window.h" + +namespace nativeapi { + +// Define Impl class with platform-specific members +class Window::Impl { +public: + Impl(HWND hwnd) : hwnd_(hwnd) {} + + HWND hwnd_; // Windows-specific handle + // Other Windows-specific state... +}; + +Window::Window() : pimpl_(std::make_unique(nullptr)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() = default; // unique_ptr handles cleanup + +void Window::Show() { + if (pimpl_->hwnd_) { + ShowWindow(pimpl_->hwnd_, SW_SHOW); + } +} + +} // namespace nativeapi +``` + +#### macOS Implementation ([platform/macos/window_macos.mm](mdc:src/platform/macos)) + +```objc +#import // Platform includes only in .mm +#include "../../window.h" + +namespace nativeapi { + +// Define Impl class with macOS-specific members +class Window::Impl { +public: + Impl(NSWindow* window) : window_(window) {} + + NSWindow* window_; // macOS-specific handle + // Other macOS-specific state... +}; + +Window::Window() : pimpl_(std::make_unique(nil)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() = default; + +void Window::Show() { + if (pimpl_->window_) { + [pimpl_->window_ makeKeyAndOrderFront:nil]; + } +} + +} // namespace nativeapi +``` + +#### Linux Implementation ([platform/linux/window_linux.cpp](mdc:src/platform/linux)) + +```cpp +#include // Platform includes only in .cpp +#include "../../window.h" + +namespace nativeapi { + +// Define Impl class with GTK-specific members +class Window::Impl { +public: + Impl(GtkWidget* window) : window_(window) {} + + GtkWidget* window_; // GTK-specific handle + // Other GTK-specific state... +}; + +Window::Window() : pimpl_(std::make_unique(nullptr)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() = default; + +void Window::Show() { + if (pimpl_->window_) { + gtk_widget_show(pimpl_->window_); + } +} + +} // namespace nativeapi +``` + +## Implementation Guidelines + +### 1. Creating a New PIMPL Class + +When adding a new cross-platform class: + +```cpp +// my_class.h +#pragma once +#include + +namespace nativeapi { + +class MyClass { +public: + MyClass(); + virtual ~MyClass(); + + void DoSomething(); + +private: + class Impl; + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi +``` + +Then create implementations for each platform: +- `src/platform/windows/my_class_windows.cpp` +- `src/platform/macos/my_class_macos.mm` +- `src/platform/linux/my_class_linux.cpp` + +### 2. Accessing Platform State + +Always access platform-specific members through `pimpl_`: + +```cpp +// Good +void Window::SetTitle(const std::string& title) { + if (pimpl_->hwnd_) { // Access through pimpl_ + SetWindowTextW(pimpl_->hwnd_, ...); + } +} + +// Bad - won't compile, hwnd_ not in public interface +void Window::SetTitle(const std::string& title) { + if (hwnd_) { // Error: no member named 'hwnd_' + ... + } +} +``` + +### 3. Constructor/Destructor Pattern + +Follow this pattern for all PIMPL classes: + +```cpp +// Header +class MyClass { +public: + MyClass(); + virtual ~MyClass(); // Virtual if used as base class + + // Copy/move operations - handle appropriately + MyClass(const MyClass&) = delete; + MyClass& operator=(const MyClass&) = delete; + +private: + class Impl; + std::unique_ptr pimpl_; +}; + +// Implementation +MyClass::MyClass() : pimpl_(std::make_unique()) {} +MyClass::~MyClass() = default; // unique_ptr handles cleanup +``` + +#### Constructor Delegation Pattern + +When a class has multiple constructors, use **delegating constructors** to avoid code duplication. The default constructor delegates to the parameterized constructor: + +```cpp +// Header +class TrayIcon { +public: + TrayIcon(); // Default constructor + TrayIcon(void* native_tray); // Wraps existing native object + virtual ~TrayIcon(); + +private: + class Impl; + std::unique_ptr pimpl_; +}; + +// Implementation - delegate to avoid duplication +TrayIcon::TrayIcon() : TrayIcon(nullptr) {} // Delegate to parameterized constructor + +TrayIcon::TrayIcon(void* tray) { + // Handle both cases in one place + NSStatusItem* status_item = nullptr; + + if (tray == nullptr) { + // Create new platform object + NSStatusBar* status_bar = [NSStatusBar systemStatusBar]; + status_item = [status_bar statusItemWithLength:NSVariableStatusItemLength]; + } else { + // Wrap existing platform object + status_item = (__bridge NSStatusItem*)tray; + } + + // All initialization logic in one place + pimpl_ = std::make_unique(status_item); + + // Additional setup that applies to both cases + if (pimpl_->status_item_) { + // Configure the status item... + } +} + +TrayIcon::~TrayIcon() = default; +``` + +**Benefits:** +- ✅ Eliminates duplicate initialization code +- ✅ Single source of truth for object setup +- ✅ Easier to maintain and update +- ✅ Prevents inconsistencies between constructors + +**Without Delegation (Don't do this):** +```cpp +// Bad - duplicated initialization logic +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) { + NSStatusBar* status_bar = [NSStatusBar systemStatusBar]; + NSStatusItem* status_item = [status_bar statusItemWithLength:NSVariableStatusItemLength]; + pimpl_->status_item_ = status_item; + + // Setup code duplicated... + if (pimpl_->status_item_) { + // Configure... + } +} + +TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique()) { + NSStatusItem* status_item = (__bridge NSStatusItem*)tray; + pimpl_->status_item_ = status_item; + + // Same setup code duplicated again! + if (pimpl_->status_item_) { + // Configure... + } +} +``` + +### 4. Manager Classes with PIMPL + +Singleton managers also use PIMPL ([window_manager.h](mdc:src/window_manager.h)): + +```cpp +class WindowManager : public EventEmitter { +public: + static WindowManager& GetInstance(); + virtual ~WindowManager(); + + std::shared_ptr Create(const WindowOptions& options); + +private: + WindowManager(); // Private constructor + + class Impl; + std::unique_ptr pimpl_; + + // Public data that doesn't vary by platform + std::unordered_map> windows_; +}; +``` + +Platform setup and cleanup methods: + +```cpp +// Header +private: + void SetupEventMonitoring(); + void CleanupEventMonitoring(); + +// Implementation forwards to pimpl_ +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + SetupEventMonitoring(); +} + +void WindowManager::SetupEventMonitoring() { + pimpl_->SetupEventMonitoring(); +} +``` + +## Common Patterns + +### Pattern 1: Wrapping Native Objects + +Constructors that accept native handles ([window.h](mdc:src/window.h)): + +```cpp +// Header +class Window { +public: + Window(); + Window(void* native_window); // Wrap existing native window +}; + +// Windows implementation +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +// macOS implementation +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} +``` + +### Pattern 2: Checking for Null Native Handles + +Always validate before using platform handles: + +```cpp +void Window::Show() { + if (!pimpl_->hwnd_) return; // Or pimpl_->window_, pimpl_->gtk_window_ + + // Safe to use handle + ShowWindow(pimpl_->hwnd_, SW_SHOW); +} +``` + +### Pattern 3: Returning Platform Handles + +Use `NativeObjectProvider` base class ([native_object_provider.h](mdc:src/foundation/native_object_provider.h)): + +```cpp +// Header +class Window : public NativeObjectProvider { +protected: + void* GetNativeObjectInternal() const override; +}; + +// Windows implementation +void* Window::GetNativeObjectInternal() const { + return static_cast(pimpl_->hwnd_); +} + +// macOS implementation +void* Window::GetNativeObjectInternal() const { + return static_cast(pimpl_->window_); +} +``` + +## Benefits of PIMPL + +1. **Binary Compatibility** - Implementation changes don't affect public API +2. **Fast Compilation** - Platform headers not included in public headers +3. **Clean Separation** - Platform code completely separated +4. **Type Safety** - Compiler ensures correct platform build +5. **No Leaks** - `unique_ptr` handles cleanup automatically + +## Best Practices + +1. **Always use `std::unique_ptr`** - Never raw pointers +2. **Define destructor in .cpp file** - Even if `= default`, needed for unique_ptr of incomplete type +3. **Keep public headers clean** - No platform includes, no platform types +4. **Validate handles** - Check for null before using native handles +5. **Use forward declarations** - Minimize includes in headers +6. **Follow naming** - Always name inner class `Impl`, always name member `pimpl_` +7. **Consider copy/move** - Usually delete copy, sometimes allow move +8. **Document constructors** - Especially those taking native handles + +## Related Patterns + +- See [Native Object Provider Rules](mdc:.cursor/rules/native-object-provider.mdc) for exposing native handles +- See [Platform Implementation Rules](mdc:.cursor/rules/platform-implementation.mdc) for platform-specific code organization +- See [Project Architecture Rules](mdc:.cursor/rules/project-architecture.mdc) for overall structure diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/platform-implementation.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/platform-implementation.mdc new file mode 100644 index 0000000..ebcb353 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/platform-implementation.mdc @@ -0,0 +1,634 @@ +--- +globs: platform/**/*.cpp,platform/**/*.mm +description: Guidelines for implementing platform-specific code for Windows, macOS, and Linux +--- + +# Platform-Specific Implementation Rules + +All platform-specific code lives in [src/platform/](mdc:src/platform) organized by operating system. Each platform implements the same cross-platform interfaces using native APIs. + +## Platform Directories + +``` +src/platform/ +├── windows/ # Windows implementation (*.cpp) +├── macos/ # macOS implementation (*.mm for Objective-C++) +└── linux/ # Linux implementation (*.cpp with GTK) +``` + +## Platform APIs Used + +| Platform | Primary APIs | Language | File Extension | +|----------|-------------|----------|----------------| +| **Windows** | Win32 API, GDI+ | C++ | `.cpp` | +| **macOS** | Cocoa/AppKit | Objective-C++ | `.mm` | +| **Linux** | GTK 3.0, X11 | C++ | `.cpp` | + +## File Naming Convention + +Platform files follow: `_.` + +Examples: +- `window_windows.cpp` - Windows window implementation +- `window_macos.mm` - macOS window implementation +- `window_linux.cpp` - Linux window implementation +- `menu_windows.cpp` - Windows menu implementation + +## Implementation Pattern + +### 1. Define Platform-Specific PIMPL::Impl + +Each platform file defines the `Impl` class declared in the cross-platform header: + +#### Windows Example ([platform/windows/window_windows.cpp](mdc:src/platform/windows)) + +```cpp +// clang-format off +#include +#include +// clang-format on +#include "../../window.h" + +namespace nativeapi { + +// Define platform-specific Impl +class Window::Impl { +public: + Impl(HWND hwnd) : hwnd_(hwnd) {} + + HWND hwnd_; // Windows window handle + // Other Windows-specific state... +}; + +// Implement constructors +Window::Window() : pimpl_(std::make_unique(nullptr)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() = default; + +// Implement methods using Win32 API +void Window::Show() { + if (pimpl_->hwnd_) { + ShowWindow(pimpl_->hwnd_, SW_SHOW); + SetForegroundWindow(pimpl_->hwnd_); + } +} + +void Window::Hide() { + if (pimpl_->hwnd_) { + ShowWindow(pimpl_->hwnd_, SW_HIDE); + } +} + +bool Window::IsVisible() const { + return pimpl_->hwnd_ && IsWindowVisible(pimpl_->hwnd_); +} + +void* Window::GetNativeObjectInternal() const { + return static_cast(pimpl_->hwnd_); +} + +} // namespace nativeapi +``` + +#### macOS Example ([platform/macos/window_macos.mm](mdc:src/platform/macos)) + +```objc +#import +#include "../../window.h" + +namespace nativeapi { + +// Define platform-specific Impl +class Window::Impl { +public: + Impl(NSWindow* window) : window_(window) { + if (window_) { + [window_ retain]; // Retain ownership + } + } + + ~Impl() { + if (window_) { + [window_ release]; // Release ownership + } + } + + NSWindow* window_; // macOS window handle +}; + +// Implement constructors +Window::Window() : pimpl_(std::make_unique(nil)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() = default; + +// Implement methods using Cocoa API +void Window::Show() { + if (pimpl_->window_) { + [pimpl_->window_ makeKeyAndOrderFront:nil]; + } +} + +void Window::Hide() { + if (pimpl_->window_) { + [pimpl_->window_ orderOut:nil]; + } +} + +bool Window::IsVisible() const { + return pimpl_->window_ && [pimpl_->window_ isVisible]; +} + +void* Window::GetNativeObjectInternal() const { + return static_cast(pimpl_->window_); +} + +} // namespace nativeapi +``` + +#### Linux Example ([platform/linux/window_linux.cpp](mdc:src/platform/linux)) + +```cpp +#include +#include "../../window.h" + +namespace nativeapi { + +// Define platform-specific Impl +class Window::Impl { +public: + Impl(GtkWidget* window) : window_(window) { + if (window_) { + g_object_ref(window_); // Increase reference count + } + } + + ~Impl() { + if (window_) { + g_object_unref(window_); // Decrease reference count + } + } + + GtkWidget* window_; // GTK window handle +}; + +// Implement constructors +Window::Window() : pimpl_(std::make_unique(nullptr)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() = default; + +// Implement methods using GTK API +void Window::Show() { + if (pimpl_->window_) { + gtk_widget_show_all(pimpl_->window_); + gtk_window_present(GTK_WINDOW(pimpl_->window_)); + } +} + +void Window::Hide() { + if (pimpl_->window_) { + gtk_widget_hide(pimpl_->window_); + } +} + +bool Window::IsVisible() const { + return pimpl_->window_ && gtk_widget_get_visible(pimpl_->window_); +} + +void* Window::GetNativeObjectInternal() const { + return static_cast(pimpl_->window_); +} + +} // namespace nativeapi +``` + +## Platform Handle Types + +### Native Handle Mapping + +| Component | Windows | macOS | Linux | +|-----------|---------|-------|-------| +| Window | `HWND` | `NSWindow*` | `GtkWidget*` (GtkWindow) | +| Menu | `HMENU` | `NSMenu*` | `GtkWidget*` (GtkMenu) | +| MenuItem | N/A (in HMENU) | `NSMenuItem*` | `GtkWidget*` (GtkMenuItem) | +| Display | `HMONITOR` | `NSScreen*` | `GdkDisplay*` | +| Image | `HBITMAP`, `Gdiplus::Bitmap*` | `NSImage*` | `GdkPixbuf*` | +| Tray Icon | `HWND` (hidden window) | `NSStatusItem*` | `AppIndicator*` | + +### Handle Lifecycle Management + +#### Windows (COM/Win32) +```cpp +// Handles are POD types, but resources need cleanup +class Window::Impl { +public: + ~Impl() { + if (hwnd_ && IsWindow(hwnd_)) { + DestroyWindow(hwnd_); + } + } + + HWND hwnd_; +}; +``` + +#### macOS (Reference Counted) +```objc +// NSObjects use reference counting +class Window::Impl { +public: + Impl(NSWindow* window) : window_(window) { + if (window_) [window_ retain]; + } + + ~Impl() { + if (window_) [window_ release]; + } + + NSWindow* window_; +}; +``` + +#### Linux (GObject Reference Counted) +```cpp +// GObjects use reference counting +class Window::Impl { +public: + Impl(GtkWidget* window) : window_(window) { + if (window_) g_object_ref(window_); + } + + ~Impl() { + if (window_) g_object_unref(window_); + } + + GtkWidget* window_; +}; +``` + +## Manager Platform-Specific Implementation + +Managers also use PIMPL for platform event monitoring: + +### Windows Manager Example + +```cpp +// window_manager_windows.cpp +// clang-format off +#include +#include +// clang-format on +#include "../../window_manager.h" + +namespace nativeapi { + +class WindowManager::Impl { +public: + Impl(WindowManager* manager) : manager_(manager) {} + + void SetupEventMonitoring() { + // Register window procedure hook + // Set up message monitoring + } + + void CleanupEventMonitoring() { + // Unregister hooks + } + + std::shared_ptr CreatePlatformWindow(const WindowOptions& options) { + // Register window class + WNDCLASSW wc = {}; + wc.lpfnWndProc = WindowProc; + // ... configure window class + + RegisterClassW(&wc); + + // Create window + HWND hwnd = CreateWindowExW(...); + + if (hwnd) { + return std::make_shared(static_cast(hwnd)); + } + + return nullptr; + } + +private: + WindowManager* manager_; + + static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + // Handle Windows messages + // Forward to manager as generic events + } +}; + +} // namespace nativeapi +``` + +### macOS Manager Example + +```objc +// window_manager_macos.mm +#import +#include "../../window_manager.h" + +@interface WindowDelegate : NSObject +@property (assign) nativeapi::WindowManager* manager; +@end + +@implementation WindowDelegate + +- (void)windowDidBecomeKey:(NSNotification*)notification { + // Forward to manager + NSWindow* window = [notification object]; + // Convert to generic event and emit +} + +- (void)windowDidMove:(NSNotification*)notification { + // Forward to manager +} + +@end + +namespace nativeapi { + +class WindowManager::Impl { +public: + Impl(WindowManager* manager) : manager_(manager) { + delegate_ = [[WindowDelegate alloc] init]; + delegate_.manager = manager; + } + + ~Impl() { + [delegate_ release]; + } + + void SetupEventMonitoring() { + // Register for NSNotifications + [[NSNotificationCenter defaultCenter] + addObserver:delegate_ + selector:@selector(windowDidBecomeKey:) + name:NSWindowDidBecomeKeyNotification + object:nil]; + } + + void CleanupEventMonitoring() { + [[NSNotificationCenter defaultCenter] removeObserver:delegate_]; + } + + std::shared_ptr CreatePlatformWindow(const WindowOptions& options) { + NSRect frame = NSMakeRect(0, 0, options.size.width, options.size.height); + + NSWindow* window = [[NSWindow alloc] + initWithContentRect:frame + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable + backing:NSBackingStoreBuffered + defer:NO]; + + [window setDelegate:delegate_]; + [window setTitle:@(options.title.c_str())]; + + return std::make_shared(static_cast(window)); + } + +private: + WindowManager* manager_; + WindowDelegate* delegate_; +}; + +} // namespace nativeapi +``` + +### Linux Manager Example + +```cpp +// window_manager_linux.cpp +#include +#include "../../window_manager.h" + +namespace nativeapi { + +class WindowManager::Impl { +public: + Impl(WindowManager* manager) : manager_(manager) {} + + void SetupEventMonitoring() { + // Connect to GTK signals + // Signals automatically dispatched by GTK main loop + } + + void CleanupEventMonitoring() { + // Disconnect signal handlers + } + + std::shared_ptr CreatePlatformWindow(const WindowOptions& options) { + GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + + gtk_window_set_title(GTK_WINDOW(window), options.title.c_str()); + gtk_window_set_default_size(GTK_WINDOW(window), + options.size.width, + options.size.height); + + // Connect signals + g_signal_connect(window, "focus-in-event", + G_CALLBACK(OnFocusIn), manager_); + g_signal_connect(window, "focus-out-event", + G_CALLBACK(OnFocusOut), manager_); + + return std::make_shared(static_cast(window)); + } + +private: + WindowManager* manager_; + + static gboolean OnFocusIn(GtkWidget* widget, GdkEvent* event, + WindowManager* manager) { + // Convert to generic event and emit + return FALSE; + } +}; + +} // namespace nativeapi +``` + +## Platform-Specific Utilities + +### String Conversion (Windows) + +Create helper files like `string_utils_windows.h`: + +```cpp +#pragma once +#include +#include + +namespace nativeapi { + +// Convert UTF-8 std::string to UTF-16 std::wstring +inline std::wstring StringToWString(const std::string& str) { + if (str.empty()) return std::wstring(); + + int size = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); + std::wstring result(size, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &result[0], size); + + return result; +} + +// Convert UTF-16 std::wstring to UTF-8 std::string +inline std::string WStringToString(const std::wstring& wstr) { + if (wstr.empty()) return std::string(); + + int size = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, + nullptr, 0, nullptr, nullptr); + std::string result(size, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, + &result[0], size, nullptr, nullptr); + + return result; +} + +} // namespace nativeapi +``` + +## Platform Detection + +Platform-specific implementations are automatically selected by the build system based on the target platform. Each platform implementation lives in its own directory: + +- **Windows**: `platform/windows/` - Uses Win32 API +- **macOS**: `platform/macos/` - Uses Cocoa/AppKit +- **Linux**: `platform/linux/` - Uses GTK 3.0 + +No preprocessor macros are needed since each platform has separate implementation files. + +### CMake Platform Selection + +[CMakeLists.txt](mdc:src/CMakeLists.txt) automatically selects platform sources: + +```cmake +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + file(GLOB PLATFORM_SOURCES "platform/linux/*.cpp") + # Link GTK, X11, etc. +elseif(APPLE) + file(GLOB PLATFORM_SOURCES "platform/macos/*.mm") + target_link_libraries(nativeapi PUBLIC "-framework Cocoa") +elseif(WIN32) + file(GLOB PLATFORM_SOURCES "platform/windows/*.cpp") + target_link_libraries(nativeapi PUBLIC user32 shell32 dwmapi gdiplus) +endif() +``` + +## Testing Platform Implementations + +Test each platform implementation: + +1. **Windows** - Build and test on Windows 10/11 +2. **macOS** - Build and test on macOS 10.15+ (Catalina or later) +3. **Linux** - Build and test on Ubuntu 20.04+ or similar + +Use example applications for manual testing: +- [examples/window_example/](mdc:examples/window_example) +- [examples/menu_example/](mdc:examples/menu_example) +- [examples/tray_icon_example/](mdc:examples/tray_icon_example) + +## Windows Platform Header Rules + +For all Windows platform implementation files, follow these header inclusion and formatting rules: + +### Header Ordering + +Always include `windows.h` before `shellapi.h`: + +```cpp +// clang-format off +#include +#include +// clang-format on +``` + +### Formatting Control + +Use `// clang-format off` and `// clang-format on` comments to disable automatic formatting for Windows-specific includes. This prevents formatters from reordering platform-specific headers that must be included in a specific order. + +### Rationale + +- `windows.h` defines fundamental Windows types and macros +- `shellapi.h` depends on definitions from `windows.h` +- Header order matters for Windows API compilation +- Disabling formatting preserves the required inclusion order + +## Best Practices + +1. **Validate handles** - Always check for null/nil/nullptr before using +2. **Manage lifetimes** - Follow platform conventions (retain/release, ref/unref, Destroy) +3. **Handle errors gracefully** - Platform APIs can fail, return sensible defaults +4. **Use platform idioms** - Message loops (Windows), Run loops (macOS), GMainLoop (Linux) +5. **Keep implementations similar** - Same structure across platforms for maintainability +6. **Document platform quirks** - Note any platform-specific behavior differences +7. **Test thoroughly** - Each platform has unique edge cases +8. **Follow Windows header ordering** - Always include `windows.h` before `shellapi.h` with formatting disabled + +## Common Pitfalls + +### ❌ Don't Mix Platform Code + +```cpp +// Bad - mixing platform APIs in same file +HWND hwnd = ...; +NSWindow* window = ...; // Error: mixing Windows and macOS APIs +``` + +```cpp +// Good - separate platform implementation files +// platform/windows/window_windows.cpp uses HWND +// platform/macos/window_macos.mm uses NSWindow* +// platform/linux/window_linux.cpp uses GtkWidget* +``` + +### ❌ Don't Leak Platform Types to Public API + +```cpp +// Bad - in window.h +class Window { +public: + HWND GetHWND(); // Platform type in public header! +}; +``` + +```cpp +// Good - use NativeObjectProvider +class Window : public NativeObjectProvider { +protected: + void* GetNativeObjectInternal() const override; // Returns void* +}; +``` + +### ❌ Don't Assume Thread Safety + +```cpp +// Bad - calling UI APIs from background thread +std::thread t([window]() { + window->Show(); // Likely to crash or behave incorrectly +}); +``` + +```cpp +// Good - marshal to UI thread +// Windows: PostMessage +// macOS: performSelectorOnMainThread +// Linux: g_idle_add +``` + +## Related Topics + +- See [PIMPL Pattern Rules](mdc:.cursor/rules/pimpl-pattern.mdc) for implementation hiding +- See [Native Object Provider Rules](mdc:.cursor/rules/native-object-provider.mdc) for exposing handles +- See [Project Architecture Rules](mdc:.cursor/rules/project-architecture.mdc) for overall structure diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/project-architecture.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/project-architecture.mdc new file mode 100644 index 0000000..f3bed6b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/project-architecture.mdc @@ -0,0 +1,184 @@ +--- +alwaysApply: true +description: Project structure, organization, and architectural patterns +--- + +# Project Architecture Rules + +This document describes the overall architecture and organization of the nativeapi project. + +## Project Overview + +**nativeapi** is a modern cross-platform C++ library providing unified access to native system APIs across Windows, macOS, and Linux. The library abstracts platform-specific details behind a clean, type-safe C++ interface and provides optional C bindings for FFI compatibility. + +## Directory Structure + +``` +nativeapi/ +├── include/ # Public API headers +│ └── nativeapi.h # Single include file for all functionality +├── src/ # Source code +│ ├── foundation/ # Core utilities (events, geometry, ID allocation) +│ ├── capi/ # C API bindings (FFI-friendly) +│ ├── platform/ # Platform-specific implementations +│ │ ├── windows/ # Windows implementations (*.cpp) +│ │ ├── macos/ # macOS implementations (*.mm) +│ │ └── linux/ # Linux implementations (*.cpp with GTK) +│ └── *.h, *.cpp # Cross-platform interface definitions +├── examples/ # Example applications +└── docs/ # Documentation +``` + +## Core Architectural Layers + +### 1. Foundation Layer ([src/foundation/](mdc:src/foundation)) + +Provides fundamental utilities used throughout the library: + +- **[event.h](mdc:src/foundation/event.h)** - Base event class with timestamps +- **[event_emitter.h](mdc:src/foundation/event_emitter.h)** - Generic event system with listeners +- **[geometry.h](mdc:src/foundation/geometry.h)** - Cross-platform geometry types (Point, Size, Rectangle) +- **[id_allocator.h](mdc:src/foundation/id_allocator.h)** - Thread-safe ID generation +- **[native_object_provider.h](mdc:src/foundation/native_object_provider.h)** - Base class for exposing native handles + +### 2. Cross-Platform Interface Layer ([src/](mdc:src)) + +Defines platform-agnostic APIs that all platforms must implement: + +- **Window Management** - [window.h](mdc:src/window.h), [window_manager.h](mdc:src/window_manager.h) +- **Display Management** - [display.h](mdc:src/display.h), [display_manager.h](mdc:src/display_manager.h) +- **Tray Icons** - [tray_icon.h](mdc:src/tray_icon.h), [tray_manager.h](mdc:src/tray_manager.h) +- **Menus** - [menu.h](mdc:src/menu.h) +- **Keyboard Monitoring** - [keyboard_monitor.h](mdc:src/keyboard_monitor.h) +- **Accessibility** - [accessibility_manager.h](mdc:src/accessibility_manager.h) +- **Events** - [window_event.h](mdc:src/window_event.h), [display_event.h](mdc:src/display_event.h), etc. + +### 3. Platform-Specific Implementation Layer + +Each platform implements the cross-platform interfaces using native APIs: + +- **Windows** - Uses Win32 API (HWND, HMENU, GDI+) +- **macOS** - Uses Cocoa/AppKit (NSWindow, NSMenu, Objective-C++) +- **Linux** - Uses GTK 3.0 (GtkWindow, GtkMenu) + +### 4. C API Layer ([src/capi/](mdc:src/capi)) + +Provides C-compatible bindings for all C++ APIs to enable FFI from other languages: + +- Each C++ API has a corresponding `_c.h` and `_c.cpp` file +- Uses opaque pointers and plain C types +- Memory management follows C conventions (explicit free functions) + +## Key Design Patterns + +### Singleton Pattern + +Managers use Meyer's singleton pattern for global access: + +```cpp +class WindowManager { +public: + static WindowManager& GetInstance(); +private: + WindowManager(); // Private constructor +}; +``` + +Examples: [WindowManager](mdc:src/window_manager.h), [DisplayManager](mdc:src/display_manager.h), [TrayManager](mdc:src/tray_manager.h) + +### PIMPL (Pointer to Implementation) + +All cross-platform classes use PIMPL to hide platform-specific details: + +```cpp +class Window { +private: + class Impl; // Forward declaration + std::unique_ptr pimpl_; // Platform-specific implementation +}; +``` + +See [PIMPL Pattern Rules](mdc:.cursor/rules/pimpl-pattern.mdc) for details. + +### Event-Driven Architecture + +All managers and many objects inherit from `EventEmitter` to provide event notifications: + +```cpp +class WindowManager : public EventEmitter { + // Can emit WindowCreatedEvent, WindowClosedEvent, etc. +}; +``` + +See [Event System Rules](mdc:.cursor/rules/event-system.mdc) for details. + +### Native Object Provider + +Classes that wrap platform-specific objects inherit from `NativeObjectProvider` to expose native handles: + +```cpp +class Window : public NativeObjectProvider { +protected: + void* GetNativeObjectInternal() const override; // Returns HWND, NSWindow*, or GtkWidget* +}; +``` + +## Build System + +The project uses CMake ([CMakeLists.txt](mdc:CMakeLists.txt)) with platform detection: + +- **C++17 Standard** required +- **Conditional Compilation** - Platform sources selected based on `CMAKE_SYSTEM_NAME` +- **Platform Dependencies**: + - Windows: user32, shell32, dwmapi, gdiplus + - macOS: Cocoa framework, Objective-C++ enabled + - Linux: GTK 3.0, X11, ayatana-appindicator + +## Naming Conventions + +### C++ API + +- **Classes**: PascalCase (e.g., `WindowManager`, `MenuItem`) +- **Methods**: PascalCase (e.g., `GetSize()`, `SetVisible()`) +- **Member Variables**: snake_case with trailing underscore (e.g., `window_id_`, `pimpl_`) +- **Enums**: PascalCase for type, PascalCase for values (e.g., `MenuItemType::Checkbox`) +- **Files**: snake_case (e.g., `window_manager.h`, `display_event.h`) + +### C API + +- **Types**: snake_case with `_t` suffix (e.g., `native_window_t`, `native_size_t`) +- **Functions**: snake_case with module prefix (e.g., `native_window_manager_create`) +- **Enums**: SCREAMING_SNAKE_CASE with prefix (e.g., `NATIVE_WINDOW_EVENT_CREATED`) +- **Files**: snake_case with `_c` suffix (e.g., `window_manager_c.h`) + +## Thread Safety + +- **Singleton Access**: Thread-safe using Meyer's singleton +- **Event System**: Thread-safe listener management and emission +- **Platform Operations**: Most operations assume single-threaded UI context +- **Global Registry**: Thread-safe with mutex protection + +## Memory Management + +- **C++ API**: Uses `std::shared_ptr` for shared ownership (windows, menus) +- **C API**: Caller manages lifetime, explicit free functions provided +- **RAII**: All classes properly clean up resources in destructors + +## Testing + +Examples serve as integration tests: +- [examples/window_example/](mdc:examples/window_example) - C++ window API +- [examples/window_c_example/](mdc:examples/window_c_example) - C window API +- [examples/menu_example/](mdc:examples/menu_example) - C++ menu API +- [examples/tray_icon_example/](mdc:examples/tray_icon_example) - C++ tray icon API + +## Best Practices + +1. **Always use PIMPL** for classes with platform-specific state +2. **Inherit from EventEmitter** for classes that emit events +3. **Inherit from NativeObjectProvider** for classes wrapping native objects +4. **Use singleton pattern** for manager classes +5. **Provide both C++ and C APIs** for all public functionality +6. **Document all public APIs** with Doxygen-style comments +7. **Test on all three platforms** before merging +8. **Follow existing naming conventions** strictly diff --git a/packages/cnativeapi/cxx_impl/.cursor/rules/singleton-managers.mdc b/packages/cnativeapi/cxx_impl/.cursor/rules/singleton-managers.mdc new file mode 100644 index 0000000..44bbf3f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.cursor/rules/singleton-managers.mdc @@ -0,0 +1,404 @@ +--- +alwaysApply: true +description: Singleton manager pattern for system-wide resource management +--- + +# Singleton Manager Pattern Rules + +System-wide resources (windows, displays, tray icons, keyboard monitoring) are managed by singleton manager classes. This ensures consistent state management and centralized event emission across the application. + +## Manager Classes + +The following managers use the singleton pattern: + +- **[WindowManager](mdc:src/window_manager.h)** - Manages all application windows +- **[DisplayManager](mdc:src/display_manager.h)** - Manages display/monitor information +- **[TrayManager](mdc:src/tray_manager.h)** - Manages system tray icons +- **[AccessibilityManager](mdc:src/accessibility_manager.h)** - Manages accessibility permissions + +## Singleton Pattern Structure + +### Meyer's Singleton Pattern + +All managers use Meyer's singleton (thread-safe in C++11+): + +```cpp +class WindowManager : public EventEmitter { +public: + // Get singleton instance + static WindowManager& GetInstance() { + static WindowManager instance; // Created on first call + return instance; + } + + virtual ~WindowManager(); + + // Prevent copying and moving + WindowManager(const WindowManager&) = delete; + WindowManager& operator=(const WindowManager&) = delete; + WindowManager(WindowManager&&) = delete; + WindowManager& operator=(WindowManager&&) = delete; + +private: + // Private constructor + WindowManager(); +}; +``` + +**Key Points:** +- Static local variable ensures single instance +- Thread-safe initialization (C++11 guarantee) +- Private constructor prevents direct instantiation +- Deleted copy/move prevents duplication +- Returns reference (not pointer) to prevent deletion + +## Complete Manager Template + +### Header File Pattern ([window_manager.h](mdc:src/window_manager.h)) + +```cpp +#pragma once +#include +#include +#include +#include "foundation/event_emitter.h" +#include "window.h" +#include "window_event.h" + +namespace nativeapi { + +class WindowManager : public EventEmitter { +public: + // Singleton access + static WindowManager& GetInstance(); + + virtual ~WindowManager(); + + // Public API + std::shared_ptr Create(const WindowOptions& options); + std::shared_ptr Get(WindowId id); + std::vector> GetAll(); + bool Destroy(WindowId id); + + // Prevent copying and moving + WindowManager(const WindowManager&) = delete; + WindowManager& operator=(const WindowManager&) = delete; + WindowManager(WindowManager&&) = delete; + WindowManager& operator=(WindowManager&&) = delete; + +private: + // Private constructor + WindowManager(); + + // PIMPL for platform-specific details + class Impl; + std::unique_ptr pimpl_; + + // Shared state (not platform-specific) + std::unordered_map> windows_; + + // Platform event monitoring + void SetupEventMonitoring(); + void CleanupEventMonitoring(); + void DispatchWindowEvent(const WindowEvent& event); +}; + +} // namespace nativeapi +``` + +### Implementation Pattern ([window_manager.cpp](mdc:src/window_manager.cpp)) + +```cpp +#include "window_manager.h" + +namespace nativeapi { + +WindowManager& WindowManager::GetInstance() { + static WindowManager instance; + return instance; +} + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + SetupEventMonitoring(); +} + +WindowManager::~WindowManager() { + CleanupEventMonitoring(); +} + +std::shared_ptr WindowManager::Create(const WindowOptions& options) { + // Platform-specific creation + auto window = pimpl_->CreatePlatformWindow(options); + + if (window) { + // Store in registry + windows_[window->GetId()] = window; + + // Emit event + Emit(window->GetId()); + } + + return window; +} + +std::shared_ptr WindowManager::Get(WindowId id) { + auto it = windows_.find(id); + return (it != windows_.end()) ? it->second : nullptr; +} + +std::vector> WindowManager::GetAll() { + std::vector> result; + result.reserve(windows_.size()); + + for (const auto& [id, window] : windows_) { + result.push_back(window); + } + + return result; +} + +bool WindowManager::Destroy(WindowId id) { + auto it = windows_.find(id); + if (it == windows_.end()) { + return false; + } + + // Platform-specific cleanup happens in Window destructor + windows_.erase(it); + + // Emit event + Emit(id); + + return true; +} + +} // namespace nativeapi +``` + +## Usage Patterns + +### Pattern 1: Accessing the Singleton + +```cpp +// Get reference to manager +auto& manager = WindowManager::GetInstance(); + +// Use manager +auto window = manager.Create(options); +``` + +**Never:** +```cpp +// Don't create pointers to singleton +WindowManager* manager = &WindowManager::GetInstance(); // Unnecessary + +// Don't try to create instances +WindowManager manager; // Won't compile - private constructor +``` + +### Pattern 2: Registering Event Listeners + +Managers inherit from `EventEmitter`, so you can add listeners: + +```cpp +auto& manager = WindowManager::GetInstance(); + +// Register listener for specific event +auto listener_id = manager.AddListener( + [](const WindowCreatedEvent& event) { + std::cout << "Window created: " << event.GetWindowId() << std::endl; + } +); + +// Register listener for all window events +auto all_listener_id = manager.AddListener( + [](const WindowEvent& event) { + std::cout << "Window event: " << event.GetTypeName() << std::endl; + } +); + +// Cleanup when done +manager.RemoveListener(listener_id); +``` + +### Pattern 3: Resource Creation and Management + +Managers create and track resources: + +```cpp +auto& manager = WindowManager::GetInstance(); + +// Create resource - manager tracks it +auto window1 = manager.Create(options1); +auto window2 = manager.Create(options2); + +// Retrieve by ID +auto window = manager.Get(window_id); + +// Get all resources +auto all_windows = manager.GetAll(); + +// Destroy resource - manager removes tracking +manager.Destroy(window_id); +``` + +### Pattern 4: Platform Event Forwarding + +Managers set up platform-specific event monitoring: + +```cpp +// In platform-specific implementation +class WindowManager::Impl { +public: + void SetupEventMonitoring() { + // Windows: Set up message hooks + // macOS: Register for NSNotifications + // Linux: Connect to GTK signals + } + + void CleanupEventMonitoring() { + // Remove hooks/observers/signal handlers + } + + void OnPlatformEvent(PlatformEventData data) { + // Convert to generic event + WindowMovedEvent event(data.window_id); + + // Dispatch through manager + manager_->DispatchWindowEvent(event); + } + +private: + WindowManager* manager_; // Back-pointer to manager +}; +``` + +## Manager Lifecycle + +### Initialization + +1. First call to `GetInstance()` creates the singleton +2. Constructor calls `SetupEventMonitoring()` to register platform callbacks +3. Manager is now ready to create and track resources + +### Operation + +1. Resources created through manager are tracked internally +2. Platform events forwarded to generic event system +3. Listeners notified of state changes + +### Cleanup + +1. When program exits, static singleton destructor runs +2. `CleanupEventMonitoring()` removes platform callbacks +3. Tracked resources cleaned up (if still alive) + +## Thread Safety + +### Thread-Safe Operations + +- **Singleton access** - `GetInstance()` is thread-safe (C++11 guarantee) +- **Event emission** - `Emit()` and listener management are thread-safe +- **Platform monitoring** - Setup/cleanup thread-safe + +### Single-Threaded Assumptions + +- **Resource operations** - `Create()`, `Get()`, `Destroy()` assume single UI thread +- **Platform APIs** - Most platform UI APIs are not thread-safe + +### Thread Safety Best Practices + +```cpp +// Safe - GetInstance() is thread-safe +auto& manager = WindowManager::GetInstance(); + +// Safe - Event listeners can be added from any thread +manager.AddListener([](const WindowEvent& e) { ... }); + +// UNSAFE - Window creation should happen on UI thread +// Use platform-specific message queue to marshal to UI thread +std::thread t([&]() { + auto window = manager.Create(options); // Potentially unsafe +}); +``` + +## Testing and Debugging + +### Singleton Lifetime Issues + +```cpp +// Be aware of static destruction order +class MyApp { + ~MyApp() { + // WindowManager might be destroyed already if MyApp is static + auto& manager = WindowManager::GetInstance(); // Use with caution + } +}; + +// Better: Clean up resources explicitly before static destruction +void MyApp::Cleanup() { + auto& manager = WindowManager::GetInstance(); + manager.Destroy(window_id_); +} +``` + +### Mocking for Tests + +To enable testing, consider factory pattern: + +```cpp +// For testability, create virtual interface +class IWindowManager { +public: + virtual ~IWindowManager() = default; + virtual std::shared_ptr Create(const WindowOptions&) = 0; + // ... other methods +}; + +// Real implementation +class WindowManager : public IWindowManager { ... }; + +// Test mock +class MockWindowManager : public IWindowManager { ... }; +``` + +## Common Pitfalls + +### ❌ Don't Take Ownership of Singleton + +```cpp +// Bad - trying to delete singleton +WindowManager* mgr = &WindowManager::GetInstance(); +delete mgr; // Undefined behavior! +``` + +### ❌ Don't Store Singleton Pointer Across Boundaries + +```cpp +// Bad - storing pointer to singleton in global +WindowManager* g_manager = &WindowManager::GetInstance(); + +// Better - get reference when needed +auto& GetManager() { + return WindowManager::GetInstance(); +} +``` + +### ❌ Don't Assume Initialization Order + +```cpp +// Bad - using singleton in static initialization +static auto window = WindowManager::GetInstance().Create(options); + +// Better - lazy initialization +std::shared_ptr GetMainWindow() { + static auto window = WindowManager::GetInstance().Create(options); + return window; +} +``` + +## Related Patterns + +- See [PIMPL Pattern Rules](mdc:.cursor/rules/pimpl-pattern.mdc) for implementation hiding +- See [Event System Rules](mdc:.cursor/rules/event-system.mdc) for event handling +- See [Platform Implementation Rules](mdc:.cursor/rules/platform-implementation.mdc) for platform-specific code diff --git a/packages/cnativeapi/cxx_impl/.github/workflows/build.yml b/packages/cnativeapi/cxx_impl/.github/workflows/build.yml new file mode 100644 index 0000000..6547703 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.github/workflows/build.yml @@ -0,0 +1,253 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + # Android builds (ARM64, ARMv7, x86_64) + build-android: + strategy: + matrix: + abi: + - arm64-v8a + - armeabi-v7a + - x86_64 + + runs-on: ubuntu-latest + name: Build Android (${{ matrix.abi }}) + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Install Android NDK + run: | + ANDROID_SDK_ROOT=/usr/local/lib/android/sdk + SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager + echo "y" | $SDKMANAGER "ndk;25.2.9519653" + echo "ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/25.2.9519653" >> $GITHUB_ENV + echo "ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/25.2.9519653" >> $GITHUB_ENV + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build + + - name: Configure CMake for Android + shell: bash + run: | + mkdir build-android + cd build-android + cmake .. \ + -DCMAKE_SYSTEM_NAME=Android \ + -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT \ + -DCMAKE_ANDROID_ARCH_ABI=${{ matrix.abi }} \ + -DCMAKE_BUILD_TYPE=Release + + - name: Build Examples + shell: bash + run: | + cd build-android + cmake --build . --config Release + + # iOS builds (device and simulator) + build-ios: + strategy: + matrix: + include: + - target: device + arch: arm64 + sysroot: iphoneos + platform_name: iOS Device + - target: simulator + arch: arm64 + sysroot: iphonesimulator + platform_name: iOS Simulator + + runs-on: macos-latest + name: Build ${{ matrix.platform_name }} + + steps: + - uses: actions/checkout@v4 + + - name: Configure CMake for iOS + shell: bash + run: | + mkdir build-ios-${{ matrix.target }} + cd build-ios-${{ matrix.target }} + cmake .. \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_SYSTEM_VERSION=14.0 \ + -DCMAKE_OSX_ARCHITECTURES=${{ matrix.arch }} \ + -DCMAKE_OSX_SYSROOT=${{ matrix.sysroot }} \ + -DCMAKE_BUILD_TYPE=Release + + - name: Build Examples + shell: bash + run: | + cd build-ios-${{ matrix.target }} + cmake --build . --config Release + + # Linux build + build-linux: + runs-on: ubuntu-latest + name: Build Linux + + steps: + - uses: actions/checkout@v4 + + - name: Set up CMake + shell: bash + run: | + cmake --version + sudo apt-get update + sudo apt-get install -y ninja-build libgtk-3-dev libx11-dev libxi-dev + + - name: Configure CMake + shell: bash + run: | + mkdir build + cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + + - name: Build Examples + shell: bash + run: | + cd build + cmake --build . --config Release + + # macOS build + build-macos: + runs-on: macos-latest + name: Build macOS + + steps: + - uses: actions/checkout@v4 + + - name: Set up CMake + shell: bash + run: cmake --version + + - name: Configure CMake + shell: bash + run: | + mkdir build + cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + + - name: Build Examples + shell: bash + run: | + cd build + cmake --build . --config Release + + # OpenHarmony builds + build-ohos: + runs-on: ubuntu-latest + name: Build OpenHarmony + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build unzip + + - name: Cache OpenHarmony Native SDK + id: cache-ohos-sdk + uses: actions/cache@v4 + with: + path: sdk + key: ohos-sdk-v5.1.0-linux-x64-${{ runner.os }} + + - name: Download OpenHarmony Native SDK + if: steps.cache-ohos-sdk.outputs.cache-hit != 'true' + run: | + echo $PWD + wget -q \ + https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.aa + wget -q \ + https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.ab + cat ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab > sdk.tar.gz + echo "Extracting native..." + mkdir sdk + tar -xzf sdk.tar.gz ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip + tar -xzf sdk.tar.gz ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip + unzip -qq ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip -d sdk + unzip -qq ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip -d sdk + ls -la sdk/native/llvm/bin/ + rm -rf ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab ohos-sdk/ sdk.tar.gz + + - name: Setup build environment + run: | + SDK_DIR=$(pwd)/sdk + + # Find the API level directory (usually "20" or similar) + API_LEVEL=$(ls -1 $SDK_DIR | grep -E '^[0-9]+$' | head -1) + if [ -z "$API_LEVEL" ]; then + # Fallback to direct native/ structure + OHOS_NATIVE_TOOLCHAIN="$SDK_DIR/native/build/cmake/ohos.toolchain.cmake" + else + OHOS_NATIVE_TOOLCHAIN="$SDK_DIR/$API_LEVEL/native/build/cmake/ohos.toolchain.cmake" + fi + + echo "OHOS_NDK_HOME=$SDK_DIR" >> $GITHUB_ENV + echo "OHOS_SDK_PATH=$SDK_DIR" >> $GITHUB_ENV + echo "OHOS_NATIVE_TOOLCHAIN=$OHOS_NATIVE_TOOLCHAIN" >> $GITHUB_ENV + + # Debug: Show SDK structure + echo "SDK structure:" + ls -la $SDK_DIR/ || true + echo "Looking for toolchain at: $OHOS_NATIVE_TOOLCHAIN" + ls -la $OHOS_NATIVE_TOOLCHAIN || true + + - name: Configure CMake for OpenHarmony + shell: bash + run: | + mkdir build-ohos + cd build-ohos + + # Configure CMake with OpenHarmony toolchain + cmake -DCMAKE_SYSTEM_NAME=OHOS \ + -DCMAKE_TOOLCHAIN_FILE="$OHOS_NATIVE_TOOLCHAIN" \ + .. + + - name: Build Examples + shell: bash + run: | + cd build-ohos + cmake --build . --config Release + + # Windows build + build-windows: + runs-on: windows-latest + name: Build Windows + + steps: + - uses: actions/checkout@v4 + + - name: Set up CMake + shell: bash + run: cmake --version + + - name: Configure CMake + shell: bash + run: | + mkdir build + cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + + - name: Build Examples + shell: bash + run: | + cd build + cmake --build . --config Release diff --git a/packages/cnativeapi/cxx_impl/.gitignore b/packages/cnativeapi/cxx_impl/.gitignore new file mode 100644 index 0000000..c975fa4 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/.gitignore @@ -0,0 +1,19 @@ +.idea/ +.vs/ +.vscode/ +build/ +build-android/ +build-ios/ +build-linux/ +build-macos/ +build-ohos/ +build-windows/ +cmake-build-debug/ +_codeql_detected_source_root + +# Generated test binaries and object files +*.o +*_test +callback_test +simple_test +event_test diff --git a/packages/cnativeapi/cxx_impl/CMakeLists.txt b/packages/cnativeapi/cxx_impl/CMakeLists.txt new file mode 100644 index 0000000..07b8cbc --- /dev/null +++ b/packages/cnativeapi/cxx_impl/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.10) + +project(nativeapi_library VERSION 0.0.1 LANGUAGES CXX C) + +include(CTest) + +# Add library subdirectory +add_subdirectory(src) + +# Add example programs subdirectory +add_subdirectory(examples/application_example) +add_subdirectory(examples/application_c_example) +add_subdirectory(examples/launch_at_login_example) +add_subdirectory(examples/launch_at_login_c_example) +add_subdirectory(examples/display_example) +add_subdirectory(examples/display_c_example) +add_subdirectory(examples/id_allocator_example) +add_subdirectory(examples/keyboard_example) +add_subdirectory(examples/menu_example) +add_subdirectory(examples/menu_c_example) +add_subdirectory(examples/message_dialog_example) +add_subdirectory(examples/message_dialog_c_example) +add_subdirectory(examples/shortcut_example) +add_subdirectory(examples/shortcut_c_example) +add_subdirectory(examples/storage_example) +add_subdirectory(examples/storage_c_example) +add_subdirectory(examples/tray_icon_example) +add_subdirectory(examples/tray_icon_c_example) +add_subdirectory(examples/url_opener_c_example) +add_subdirectory(examples/window_c_example) +add_subdirectory(examples/window_example) + +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/packages/cnativeapi/cxx_impl/LICENSE b/packages/cnativeapi/cxx_impl/LICENSE new file mode 100644 index 0000000..e22a797 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cnativeapi/cxx_impl/README.md b/packages/cnativeapi/cxx_impl/README.md new file mode 100644 index 0000000..9506f29 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/README.md @@ -0,0 +1,82 @@ +# nativeapi + +A modern cross-platform C++ library providing seamless, unified access to native system APIs across multiple platforms. + +🚧 Work in Progress: This package is currently under active development. + +## Requirements + +### Build Requirements + +- CMake 3.10 or later +- C++17 compatible compiler: + - Windows: Visual Studio 2017 or later / MinGW-w64 + - macOS: Xcode 9.0 or later (Clang) + - Linux: GCC 7.0+ or Clang 5.0+ + +### Platform-specific Dependencies + +#### Linux + +- GTK 3.0 development headers + +```bash +# Ubuntu/Debian +sudo apt-get install libgtk-3-dev + +# CentOS/RHEL/Fedora +sudo yum install gtk3-devel +# or +sudo dnf install gtk3-devel +``` + +#### macOS + +- Cocoa framework (included with Xcode) + +#### Windows + +- Windows SDK + +## Building from Source + +### Quick Start + +```bash +# Clone the repository +git clone https://github.com/libnativeapi/nativeapi.git +cd nativeapi +``` + +```bash +# Create build directory +mkdir build +cd build + +# Configure and build +cmake .. +cmake --build . --config Release +``` + +## Development + +### Code Formatting + +Format the codebase using clang-format: + +```bash +clang-format -i **/*.cpp **/*.h **/*.mm +``` + +## Language Bindings + +Currently available language bindings for nativeapi: + +- [nativeapi-flutter](https://github.com/libnativeapi/nativeapi-flutter) - Flutter bindings +- [nativeapi-swift](https://github.com/leanflutter/nativeapi-swift) - Swift bindings + +These bindings provide native system API access while preserving the library's core functionality. + +## License + +[MIT](./LICENSE) diff --git a/packages/cnativeapi/cxx_impl/examples/application_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/application_c_example/CMakeLists.txt new file mode 100644 index 0000000..8d1736b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/application_c_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(application_c_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(application_c_example + "main.c" +) + +# Link main library +target_link_libraries(application_c_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(application_c_example PROPERTIES + OUTPUT_NAME "application_c_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/application_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/application_c_example/main.c new file mode 100644 index 0000000..e8a3e7c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/application_c_example/main.c @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "nativeapi.h" + +void on_application_event(const native_application_event_t* event, void* user_data) { + (void)user_data; + switch (event->type) { + case NATIVE_APPLICATION_EVENT_TYPE_STARTED: + printf("Application started event received\n"); + break; + case NATIVE_APPLICATION_EVENT_TYPE_EXITING: + printf("Application exiting event received with exit code: %d\n", + event->data.exiting.exit_code); + break; + case NATIVE_APPLICATION_EVENT_TYPE_ACTIVATED: + printf("Application activated event received\n"); + break; + case NATIVE_APPLICATION_EVENT_TYPE_DEACTIVATED: + printf("Application deactivated event received\n"); + break; + case NATIVE_APPLICATION_EVENT_TYPE_QUIT_REQUESTED: + printf("Application quit requested event received\n"); + break; + default: + printf("Unknown application event type: %d\n", event->type); + break; + } +} + +int main() { + printf("Application C API Example\n"); + + // Application is a singleton on the C++ side, so its C functions take no + // receiver argument. + printf("Single instance: %s\n", native_application_is_single_instance() ? "Yes" : "No"); + + native_listener_id_t listener_id = native_application_add_listener(on_application_event, NULL); + if (listener_id == NATIVE_INVALID_LISTENER_ID) { + fprintf(stderr, "Failed to add event listener\n"); + return 1; + } + + // Create a simple window with default settings + native_window_t window = native_window_create(); + if (window == NATIVE_INVALID_WINDOW) { + fprintf(stderr, "Failed to create window\n"); + return 1; + } + + // Configure the window + native_window_set_title(window, "Application C Example Window"); + native_size_t size = {400.0, 300.0}; + native_window_set_size(window, size, false); + + printf("Window created successfully\n"); + printf("Window ID: %u\n", native_window_get_id(window)); + + // Show the window + native_window_show(window); + + printf("Starting application event loop...\n"); + printf("Press Ctrl+C to quit\n"); + + int exit_code = native_application_run(); + + printf("Application exited with code: %d\n", exit_code); + + native_application_remove_listener(listener_id); + native_window_free(window); + + return exit_code; +} diff --git a/packages/cnativeapi/cxx_impl/examples/application_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/application_example/CMakeLists.txt new file mode 100644 index 0000000..a8afd5d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/application_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(application_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(application_example + "main.cpp" +) + +# Link main library +target_link_libraries(application_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(application_example PROPERTIES + OUTPUT_NAME "application_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/application_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/application_example/main.cpp new file mode 100644 index 0000000..ef26182 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/application_example/main.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "nativeapi.h" + +using namespace nativeapi; + +int main() { + std::cout << "Application Example" << std::endl; + + // Get the Application singleton + auto& app = Application::GetInstance(); + + std::cout << "Application initialized automatically" << std::endl; + std::cout << "Single instance: " << (app.IsSingleInstance() ? "Yes" : "No") << std::endl; + + // Add event listeners + auto started_listener = + app.AddListener([](const ApplicationStartedEvent& event) { + std::cout << "Application started event received" << std::endl; + }); + + auto quit_listener = app.AddListener( + [](const ApplicationQuitRequestedEvent& event) { + std::cout << "Application quit requested event received" << std::endl; + }); + + auto activated_listener = + app.AddListener([](const ApplicationActivatedEvent& event) { + std::cout << "Application activated event received" << std::endl; + }); + + auto deactivated_listener = + app.AddListener([](const ApplicationDeactivatedEvent& event) { + std::cout << "Application deactivated event received" << std::endl; + }); + + // Create a simple window (automatically registered) + auto& window_manager = WindowManager::GetInstance(); + + auto window = std::make_shared(); + window->SetTitle("Application Example Window"); + + if (!window) { + std::cerr << "Failed to create window" << std::endl; + return 1; + } + + // Set as primary window + app.SetPrimaryWindow(window); + + std::cout << "Window created successfully" << std::endl; + std::cout << "Window ID: " << window->GetId() << std::endl; + + // Show the window + window->Show(); + + std::cout << "Starting application event loop..." << std::endl; + std::cout << "Press Ctrl+C to quit" << std::endl; + + // Run the application + int exit_code = app.Run(); + + std::cout << "Application exited with code: " << exit_code << std::endl; + + // Clean up listeners + app.RemoveListener(started_listener); + app.RemoveListener(quit_listener); + app.RemoveListener(activated_listener); + app.RemoveListener(deactivated_listener); + + return exit_code; +} diff --git a/packages/cnativeapi/cxx_impl/examples/display_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/display_c_example/CMakeLists.txt new file mode 100644 index 0000000..a3bca3f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/display_c_example/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10) + +project(display_c_example) + +set(CMAKE_C_STANDARD 99) + +add_executable(display_c_example main.c) + +target_link_libraries(display_c_example nativeapi) + +target_include_directories(display_c_example PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) diff --git a/packages/cnativeapi/cxx_impl/examples/display_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/display_c_example/main.c new file mode 100644 index 0000000..8de87d4 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/display_c_example/main.c @@ -0,0 +1,108 @@ +#include +#include + +// Include only C API headers +#include "../../src/capi/display_c.h" +#include "../../src/capi/display_manager_c.h" +#include "../../src/capi/geometry_c.h" +#include "../../src/capi/string_utils_c.h" + +int main() { + printf("=== Native API C Display Example ===\n\n"); + + // Test getting all displays + native_display_list_t display_list = native_display_manager_get_all(); + + if (display_list.displays != NULL && display_list.count > 0) { + printf("Found %ld display(s):\n\n", display_list.count); + + for (size_t i = 0; i < display_list.count; i++) { + native_display_t display = display_list.displays[i]; + + printf("Display %zu:\n", i + 1); + + // Use getter functions for all properties + char* name = native_display_get_name(display); + printf(" Name: %s\n", name ? name : "Unknown"); + free_c_str(name); + + native_display_id_t id = native_display_get_id(display); + printf(" ID: %u\n", id); + + native_point_t position = native_display_get_position(display); + printf(" Position: (%.0f, %.0f)\n", position.x, position.y); + + native_size_t size = native_display_get_size(display); + printf(" Size: %.0f x %.0f\n", size.width, size.height); + + native_rectangle_t work_area = native_display_get_work_area(display); + printf(" Work Area: (%.0f, %.0f) %.0f x %.0f\n", work_area.x, work_area.y, work_area.width, + work_area.height); + + double scale_factor = native_display_get_scale_factor(display); + printf(" Scale Factor: %.2f\n", scale_factor); + + bool is_primary = native_display_is_primary(display); + printf(" Primary: %s\n", is_primary ? "Yes" : "No"); + + // Display orientation + printf(" Orientation: "); + native_display_orientation_t orientation = native_display_get_orientation(display); + switch (orientation) { + case NATIVE_DISPLAY_ORIENTATION_PORTRAIT: + printf("Portrait (0°)"); + break; + case NATIVE_DISPLAY_ORIENTATION_LANDSCAPE: + printf("Landscape (90°)"); + break; + case NATIVE_DISPLAY_ORIENTATION_PORTRAIT_FLIPPED: + printf("Portrait Flipped (180°)"); + break; + case NATIVE_DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: + printf("Landscape Flipped (270°)"); + break; + default: + printf("Unknown"); + break; + } + printf("\n"); + + int refresh_rate = native_display_get_refresh_rate(display); + printf(" Refresh Rate: %d Hz\n", refresh_rate); + + int bit_depth = native_display_get_bit_depth(display); + printf(" Bit Depth: %d bits\n", bit_depth); + + printf("\n"); + } + + // Clean up memory + native_display_list_free(&display_list); + } else { + printf("No displays found or error occurred\n"); + } + + // Test getting primary display + printf("=== Primary Display ===\n"); + native_display_t primary = native_display_manager_get_primary(); + if (primary) { + char* name = native_display_get_name(primary); + printf("Primary display: %s\n", name ? name : "Unknown"); + free_c_str(name); + + native_size_t size = native_display_get_size(primary); + printf("Size: %.0f x %.0f\n", size.width, size.height); + + // Free the primary display handle + native_display_free(primary); + } else { + printf("Failed to get primary display\n"); + } + + // Test getting cursor position + printf("\n=== Cursor Position ===\n"); + native_point_t cursor_pos = native_display_manager_get_cursor_position(); + printf("Cursor position: (%.0f, %.0f)\n", cursor_pos.x, cursor_pos.y); + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/display_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/display_example/CMakeLists.txt new file mode 100644 index 0000000..bab3abe --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/display_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(display_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(display_example + "main.cpp" +) + +# Link main library +target_link_libraries(display_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(display_example PROPERTIES + OUTPUT_NAME "display_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/display_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/display_example/main.cpp new file mode 100644 index 0000000..988b87d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/display_example/main.cpp @@ -0,0 +1,116 @@ +#include +#include +#include +#include "nativeapi.h" + +using nativeapi::Display; +using nativeapi::DisplayManager; +using nativeapi::DisplayOrientation; +using nativeapi::Point; +using nativeapi::Rectangle; +using nativeapi::Size; + +// Helper function to convert orientation enum to string +std::string orientationToString(DisplayOrientation orientation) { + switch (orientation) { + case DisplayOrientation::kPortrait: + return "Portrait (0°)"; + case DisplayOrientation::kLandscape: + return "Landscape (90°)"; + case DisplayOrientation::kPortraitFlipped: + return "Portrait Flipped (180°)"; + case DisplayOrientation::kLandscapeFlipped: + return "Landscape Flipped (270°)"; + default: + return "Unknown"; + } +} + +int main() { + try { + std::cout << "=== Native API C++ Display Example ===" << std::endl << std::endl; + + DisplayManager& displayManager = DisplayManager::GetInstance(); + + // Test getting all displays + std::vector> displays = displayManager.GetAll(); + + if (!displays.empty()) { + std::cout << "Found " << displays.size() << " display(s):" << std::endl << std::endl; + + for (size_t i = 0; i < displays.size(); i++) { + const Display& display = *displays[i]; + + std::cout << "Display " << (i + 1) << ":" << std::endl; + + // Name + std::cout << " Name: " << display.GetName() << std::endl; + + // ID + std::cout << " ID: " << display.GetId() << std::endl; + + // Position + Point position = display.GetPosition(); + std::cout << " Position: (" << (int)position.x << ", " << (int)position.y << ")" + << std::endl; + + // Size + Size size = display.GetSize(); + std::cout << " Size: " << (int)size.width << " x " << (int)size.height << std::endl; + + // Work Area + Rectangle workArea = display.GetWorkArea(); + std::cout << " Work Area: (" << (int)workArea.x << ", " << (int)workArea.y << ") " + << (int)workArea.width << " x " << (int)workArea.height << std::endl; + + // Scale Factor + std::cout << " Scale Factor: " << std::fixed << std::setprecision(2) + << display.GetScaleFactor() << std::endl; + + // Primary + std::cout << " Primary: " << (display.IsPrimary() ? "Yes" : "No") << std::endl; + + // Orientation + std::cout << " Orientation: " << orientationToString(display.GetOrientation()) + << std::endl; + + // Refresh Rate + std::cout << " Refresh Rate: " << display.GetRefreshRate() << " Hz" << std::endl; + + // Bit Depth + std::cout << " Bit Depth: " << display.GetBitDepth() << " bits" << std::endl; + + std::cout << std::endl; + } + } else { + std::cout << "No displays found or error occurred" << std::endl; + } + + // Test getting primary display + std::cout << "=== Primary Display ===" << std::endl; + std::shared_ptr primary = displayManager.GetPrimary(); + if (primary) { + std::cout << "Primary display: " << primary->GetName() << std::endl; + + Size size = primary->GetSize(); + std::cout << "Size: " << (int)size.width << " x " << (int)size.height << std::endl; + } else { + std::cout << "No primary display available" << std::endl; + } + + // Test getting cursor position + std::cout << std::endl << "=== Cursor Position ===" << std::endl; + Point cursorPos = displayManager.GetCursorPosition(); + std::cout << "Cursor position: (" << (int)cursorPos.x << ", " << (int)cursorPos.y << ")" + << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown error occurred" << std::endl; + return 1; + } + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/id_allocator_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/id_allocator_example/CMakeLists.txt new file mode 100644 index 0000000..9d9637b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/id_allocator_example/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Create executable for ID allocator example +add_executable(id_allocator_example main.cpp) + +# Link against the nativeapi library +target_link_libraries(id_allocator_example nativeapi) diff --git a/packages/cnativeapi/cxx_impl/examples/id_allocator_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/id_allocator_example/main.cpp new file mode 100644 index 0000000..306a54e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/id_allocator_example/main.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include +#include +#include "../../src/foundation/id_allocator.h" + +using namespace nativeapi; + +// Window, Menu, MenuItem, TrayIcon and Display come from the IdTypeTag registry +// in id_allocator.h. They are only ever used as template arguments here, so the +// forward declarations that header already provides are enough — no need to pull +// in the full class definitions. +// +// This example used to declare its own placeholder structs with these names. +// That worked back when type tags were assigned by a runtime counter and any +// type at all could be passed. Tags are now compile-time constants from a +// central registry, so a type must be registered to be allocatable. + +int main() { + std::cout << "IdAllocator Template Example" << std::endl; + std::cout << "============================" << std::endl; + + // Example 1: Basic allocation for different object types + std::cout << "\n1. Basic Allocation:" << std::endl; + + auto window_id = IdAllocator::Allocate(); + auto menu_id = IdAllocator::Allocate(); + auto tray_id = IdAllocator::Allocate(); + + std::cout << "Window ID: 0x" << std::hex << window_id << std::dec + << " (Type: " << IdAllocator::GetType(window_id) + << ", Sequence: " << IdAllocator::GetSequence(window_id) << ")" << std::endl; + + std::cout << "Menu ID: 0x" << std::hex << menu_id << std::dec + << " (Type: " << IdAllocator::GetType(menu_id) + << ", Sequence: " << IdAllocator::GetSequence(menu_id) << ")" << std::endl; + + std::cout << "Tray ID: 0x" << std::hex << tray_id << std::dec + << " (Type: " << IdAllocator::GetType(tray_id) + << ", Sequence: " << IdAllocator::GetSequence(tray_id) << ")" << std::endl; + + // Example 2: TryAllocate with error checking + std::cout << "\n2. TryAllocate (safer allocation):" << std::endl; + + auto maybe_id = IdAllocator::TryAllocate(); + if (maybe_id != IdAllocator::kInvalidId) { + std::cout << "MenuItem ID allocated successfully: 0x" << std::hex << maybe_id << std::dec + << std::endl; + } else { + std::cout << "MenuItem ID allocation failed" << std::endl; + } + + // Example 3: ID validation and decomposition + std::cout << "\n3. ID Validation and Decomposition:" << std::endl; + + std::cout << "Is window_id valid? " << (IdAllocator::IsValid(window_id) ? "Yes" : "No") + << std::endl; + + auto decomposed = IdAllocator::Decompose(window_id); + std::cout << "Window ID decomposed - Type: " << decomposed.first + << ", Sequence: " << decomposed.second << std::endl; + + // Example 4: Current count query + std::cout << "\n4. Current Counter Query:" << std::endl; + + std::cout << "Current Window counter (before allocation): " + << IdAllocator::GetCurrentCount() << std::endl; + + auto new_window_id = IdAllocator::Allocate(); + std::cout << "New Window ID: 0x" << std::hex << new_window_id << std::dec + << " (Sequence: " << IdAllocator::GetSequence(new_window_id) << ")" << std::endl; + + std::cout << "Current Window counter (after allocation): " + << IdAllocator::GetCurrentCount() << std::endl; + + // Example 5: Multiple allocations + std::cout << "\n5. Multiple Allocations:" << std::endl; + + std::vector window_ids; + for (int i = 0; i < 5; ++i) { + window_ids.push_back(IdAllocator::Allocate()); + } + + std::cout << "Allocated " << window_ids.size() << " Window IDs:" << std::endl; + for (size_t i = 0; i < window_ids.size(); ++i) { + std::cout << " ID " << (i + 1) << ": 0x" << std::hex << window_ids[i] << std::dec + << " (Sequence: " << IdAllocator::GetSequence(window_ids[i]) << ")" << std::endl; + } + + // Example 6: Thread safety demonstration + std::cout << "\n6. Thread Safety Demonstration:" << std::endl; + + std::vector threads; + std::vector thread_ids; + std::mutex ids_mutex; + + const int num_threads = 3; + const int ids_per_thread = 10; + + auto start_time = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back([&thread_ids, &ids_mutex, ids_per_thread, i]() { + std::vector local_ids; + + for (int j = 0; j < ids_per_thread; ++j) { + auto id = IdAllocator::Allocate(); + local_ids.push_back(id); + } + + { + std::lock_guard lock(ids_mutex); + thread_ids.insert(thread_ids.end(), local_ids.begin(), local_ids.end()); + } + }); + } + + for (auto& thread : threads) { + thread.join(); + } + + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + + std::cout << "Allocated " << thread_ids.size() << " Display IDs from " << num_threads + << " threads in " << duration.count() << " microseconds" << std::endl; + + // Verify all IDs are unique + std::sort(thread_ids.begin(), thread_ids.end()); + auto it = std::unique(thread_ids.begin(), thread_ids.end()); + std::cout << "All IDs are unique: " << (it == thread_ids.end() ? "Yes" : "No") << std::endl; + + // Example 7: Different object types + std::cout << "\n7. Different Object Types:" << std::endl; + + auto display_id = IdAllocator::Allocate(); + + std::cout << "Display ID: 0x" << std::hex << display_id << std::dec + << " (Type: " << IdAllocator::GetType(display_id) << ")" << std::endl; + + // Example 8: Reset functionality (for testing) + std::cout << "\n8. Reset Functionality:" << std::endl; + + std::cout << "Menu counter before reset: " << IdAllocator::GetCurrentCount() << std::endl; + IdAllocator::Reset(); + std::cout << "Menu counter after reset: " << IdAllocator::GetCurrentCount() << std::endl; + + auto new_menu_id = IdAllocator::Allocate(); + std::cout << "New Menu ID after reset: 0x" << std::hex << new_menu_id << std::dec + << " (Sequence: " << IdAllocator::GetSequence(new_menu_id) << ")" << std::endl; + + // Example 9: Independent types after reset + std::cout << "\n9. Independent Types After Reset:" << std::endl; + + auto window_after_reset = IdAllocator::Allocate(); + std::cout << "Window ID after Menu reset: 0x" << std::hex << window_after_reset << std::dec + << " (Sequence: " << IdAllocator::GetSequence(window_after_reset) << ")" << std::endl; + std::cout << "Window counter was not affected by Menu reset" << std::endl; + + std::cout << "\nExample completed successfully!" << std::endl; + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/keyboard_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/keyboard_example/CMakeLists.txt new file mode 100644 index 0000000..17ad22c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/keyboard_example/CMakeLists.txt @@ -0,0 +1,19 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +project(keyboard_example) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add executable +add_executable(keyboard_example main.cpp) + +# Link with the native API library +target_link_libraries(keyboard_example nativeapi) + +# Set include directories to find the C API header +target_include_directories(keyboard_example PRIVATE ../../src) diff --git a/packages/cnativeapi/cxx_impl/examples/keyboard_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/keyboard_example/main.cpp new file mode 100644 index 0000000..a94b129 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/keyboard_example/main.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include + +#include "../../src/capi/keyboard_c.h" +#include "../../src/capi/keyboard_monitor_c.h" + +// Global monitor handle for cleanup +static native_keyboard_monitor_t g_monitor = NATIVE_INVALID_KEYBOARD_MONITOR; +static bool g_running = true; + +// Signal handler for graceful shutdown +void signal_handler(int sig) { + std::cout << "\nReceived signal " << sig << ", shutting down...\n"; + g_running = false; + if (g_monitor != NATIVE_INVALID_KEYBOARD_MONITOR) { + native_keyboard_monitor_stop(g_monitor); + native_keyboard_monitor_free(g_monitor); + g_monitor = NATIVE_INVALID_KEYBOARD_MONITOR; + } + exit(0); +} + +// A KeyboardMonitor emits one KeyboardEvent stream, tagged by concrete type. +void on_keyboard_event(const native_keyboard_event_t* event, void* user_data) { + (void)user_data; + switch (event->type) { + case NATIVE_KEYBOARD_EVENT_TYPE_KEY_PRESSED: + std::cout << "Key pressed: " << event->keycode << std::endl; + break; + case NATIVE_KEYBOARD_EVENT_TYPE_KEY_RELEASED: + std::cout << "Key released: " << event->keycode << std::endl; + break; + case NATIVE_KEYBOARD_EVENT_TYPE_MODIFIER_KEYS_CHANGED: { + unsigned int modifier_keys = event->data.modifier_keys_changed.modifier_keys; + std::cout << "Modifier keys changed: 0x" << std::hex << modifier_keys << std::dec; + + if (modifier_keys & NATIVE_MODIFIER_KEY_SHIFT) + std::cout << " SHIFT"; + if (modifier_keys & NATIVE_MODIFIER_KEY_CTRL) + std::cout << " CTRL"; + if (modifier_keys & NATIVE_MODIFIER_KEY_ALT) + std::cout << " ALT"; + if (modifier_keys & NATIVE_MODIFIER_KEY_META) + std::cout << " META"; + if (modifier_keys & NATIVE_MODIFIER_KEY_FN) + std::cout << " FN"; + if (modifier_keys & NATIVE_MODIFIER_KEY_CAPS_LOCK) + std::cout << " CAPS"; + if (modifier_keys & NATIVE_MODIFIER_KEY_NUM_LOCK) + std::cout << " NUM"; + if (modifier_keys & NATIVE_MODIFIER_KEY_SCROLL_LOCK) + std::cout << " SCROLL"; + + std::cout << std::endl; + break; + } + } +} + +int main() { + std::cout << "KeyboardMonitor C API Example\n"; + std::cout << "==============================\n"; + + // Set up signal handlers + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Create keyboard monitor + g_monitor = native_keyboard_monitor_create(); + if (g_monitor == NATIVE_INVALID_KEYBOARD_MONITOR) { + std::cout << "Failed to create keyboard monitor\n"; + return 1; + } + std::cout << "Keyboard monitor created successfully\n"; + + // Register the single event listener + if (native_keyboard_monitor_add_listener(g_monitor, on_keyboard_event, nullptr) == + NATIVE_INVALID_LISTENER_ID) { + std::cout << "Failed to add listener\n"; + native_keyboard_monitor_free(g_monitor); + return 1; + } + std::cout << "Listener registered successfully\n"; + + // Start monitoring + native_keyboard_monitor_start(g_monitor); + + if (native_keyboard_monitor_is_monitoring(g_monitor)) { + std::cout << "Keyboard monitoring is now active\n"; + std::cout << "\nThis example demonstrates the KeyboardMonitor C API:\n"; + std::cout << "• native_keyboard_monitor_create() - Creates a monitor instance\n"; + std::cout << "• native_keyboard_monitor_add_listener() - Registers the event listener\n"; + std::cout << "• native_keyboard_monitor_start() - Starts monitoring\n"; + std::cout << "• native_keyboard_monitor_is_monitoring() - Checks status\n"; + std::cout << "• native_keyboard_monitor_stop() - Stops monitoring\n"; + std::cout << "• native_keyboard_monitor_free() - Releases the handle\n"; + std::cout << "\nPress keys to see events. Press Ctrl+C to exit.\n\n"; + } else { + std::cout << "Warning: Monitor created but not monitoring (may be due to " + "permissions or display server)\n"; + std::cout << "This is expected in headless environments or without proper " + "permissions.\n"; + std::cout << "On a desktop system with X11/Wayland, you would see keyboard " + "events.\n\n"; + } + + // Keep the main thread alive to receive events + while (g_running) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + return 0; +} \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/CMakeLists.txt new file mode 100644 index 0000000..b6cd83d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.10) + +project(launch_at_login_c_example) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Add executable +add_executable(launch_at_login_c_example main.c) + +# Link with the native API library +target_link_libraries(launch_at_login_c_example nativeapi) + +# Set include directories +target_include_directories(launch_at_login_c_example PRIVATE ../../include) diff --git a/packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/main.c new file mode 100644 index 0000000..3549ffe --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/launch_at_login_c_example/main.c @@ -0,0 +1,129 @@ +#include +#include +#include + +int main(void) { + printf("LaunchAtLogin C API Example\n"); + printf("=======================\n\n"); + + /* Check if launch-at-login is supported on this platform */ + if (!native_launch_at_login_is_supported()) { + printf("LaunchAtLogin is not supported on this platform.\n"); + return 0; + } + + printf("LaunchAtLogin is supported on this platform.\n\n"); + +#if defined(__APPLE__) + /* + * On macOS, the default constructor registers the main app with SMAppService. + * Custom identifiers are for bundled login item helpers. + */ + native_launch_at_login_t launch_at_login = native_launch_at_login_create(); +#else + /* Create a LaunchAtLogin manager with a custom identifier and display name */ + native_launch_at_login_t launch_at_login = + native_launch_at_login_create_with_id_and_display_name("com.example.myapp.c", + "My C Example App"); +#endif + if (launch_at_login == NATIVE_INVALID_LAUNCH_AT_LOGIN) { + printf("Failed to create LaunchAtLogin instance.\n"); + return 1; + } + + /* Display current configuration */ + char* id = native_launch_at_login_get_id(launch_at_login); + char* display_name = native_launch_at_login_get_display_name(launch_at_login); + char* executable = native_launch_at_login_get_executable_path(launch_at_login); + + printf("LaunchAtLogin configuration:\n"); + printf(" ID: %s\n", id ? id : "(null)"); + printf(" Display name: %s\n", display_name ? display_name : "(null)"); + printf(" Executable: %s\n\n", executable ? executable : "(null)"); + + free_c_str(id); + free_c_str(display_name); + +#if !defined(__APPLE__) + /* Set a custom program path and arguments */ + char* argument_items[] = {"--minimized", "--launch_at_login"}; + native_string_list_t arguments = {argument_items, 2}; + native_launch_at_login_set_program(launch_at_login, executable ? executable : "", arguments); +#endif + free_c_str(executable); + + /* Retrieve and display the updated executable path */ + char* exec_path = native_launch_at_login_get_executable_path(launch_at_login); + printf("After SetProgram:\n"); + printf(" Executable: %s\n", exec_path ? exec_path : "(null)"); +#if defined(__APPLE__) + printf(" Arguments: (not supported by macOS SMAppService main-app login items)\n\n"); +#else + printf(" Arguments: --minimized --launch_at_login\n\n"); +#endif + free_c_str(exec_path); + + /* Check current state before enabling */ + printf("Is enabled (before Enable): %s\n", + native_launch_at_login_is_enabled(launch_at_login) ? "yes" : "no"); + + /* Enable launch-at-login */ + printf("Enabling launch-at-login...\n"); + if (native_launch_at_login_enable(launch_at_login)) { + printf("Launch-at-login enabled successfully.\n"); + } else { + printf("Failed to enable launch-at-login.\n"); + native_launch_at_login_free(launch_at_login); + return 1; + } + + /* Verify it is now enabled */ + printf("Is enabled (after Enable): %s\n\n", + native_launch_at_login_is_enabled(launch_at_login) ? "yes" : "no"); + + /* Update the display name and re-enable to update the stored entry */ + native_launch_at_login_set_display_name(launch_at_login, "My C Example App (Updated)"); + char* updated_name = native_launch_at_login_get_display_name(launch_at_login); + printf("Updated display name to: %s\n", updated_name ? updated_name : "(null)"); + free_c_str(updated_name); + native_launch_at_login_enable(launch_at_login); + + /* Disable launch-at-login */ + printf("\nDisabling launch-at-login...\n"); + if (native_launch_at_login_disable(launch_at_login)) { + printf("Launch-at-login disabled successfully.\n"); + } else { + printf("Failed to disable launch-at-login.\n"); + native_launch_at_login_free(launch_at_login); + return 1; + } + + /* Verify it is now disabled */ + printf("Is enabled (after Disable): %s\n\n", + native_launch_at_login_is_enabled(launch_at_login) ? "yes" : "no"); + + /* Clean up */ + native_launch_at_login_free(launch_at_login); + + printf("Example completed successfully!\n\n"); + printf("This example demonstrated:\n"); + printf(" * native_launch_at_login_is_supported() - Check platform support\n"); + printf(" * native_launch_at_login_create_with_id_and_display_name() - Create with ID and name\n"); + printf(" * native_launch_at_login_get_id() - Get identifier\n"); + printf(" * native_launch_at_login_get_display_name() - Get display name\n"); + printf(" * native_launch_at_login_set_display_name() - Update display name\n"); + printf(" * native_launch_at_login_set_program() - Set executable and arguments\n"); + printf(" * native_launch_at_login_get_executable_path() - Get configured executable\n"); + printf( + " * native_launch_at_login_enable() - Register launch-at-login with the " + "OS\n"); + printf( + " * native_launch_at_login_disable() - Remove launch-at-login from the " + "OS\n"); + printf( + " * native_launch_at_login_is_enabled() - Query current registration " + "state\n"); + printf(" * native_launch_at_login_free() - Free resources\n"); + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/launch_at_login_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/launch_at_login_example/CMakeLists.txt new file mode 100644 index 0000000..c4f095c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/launch_at_login_example/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.10) + +project(launch_at_login_example) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add executable +add_executable(launch_at_login_example main.cpp) + +# Link with the native API library +target_link_libraries(launch_at_login_example nativeapi) + +# Set include directories +target_include_directories(launch_at_login_example PRIVATE ../../include) diff --git a/packages/cnativeapi/cxx_impl/examples/launch_at_login_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/launch_at_login_example/main.cpp new file mode 100644 index 0000000..b278d81 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/launch_at_login_example/main.cpp @@ -0,0 +1,101 @@ +#include +#include +#include + +#include "nativeapi.h" + +using namespace nativeapi; + +int main() { + std::cout << "LaunchAtLogin Example\n"; + std::cout << "=================\n\n"; + + // Check if launch-at-login is supported on this platform + if (!LaunchAtLogin::IsSupported()) { + std::cout << "LaunchAtLogin is not supported on this platform.\n"; + return 0; + } + + std::cout << "LaunchAtLogin is supported on this platform.\n\n"; + + // On macOS, the default constructor registers the main app with SMAppService. + // Custom identifiers are for bundled login item helpers. +#if defined(__APPLE__) + LaunchAtLogin launch_at_login; +#else + LaunchAtLogin launch_at_login("com.example.myapp", "My Example App"); +#endif + + // Display current configuration + std::cout << "LaunchAtLogin configuration:\n"; + std::cout << " ID: " << launch_at_login.GetId() << "\n"; + std::cout << " Display name: " << launch_at_login.GetDisplayName() << "\n"; + std::cout << " Executable: " << launch_at_login.GetExecutablePath() << "\n\n"; + +// macOS SMAppService main-app login items do not support arbitrary arguments. +#if !defined(__APPLE__) + // Set a custom program path and arguments + launch_at_login.SetProgram(launch_at_login.GetExecutablePath(), + {"--minimized", "--launch_at_login"}); +#endif + + std::cout << "After SetProgram:\n"; + std::cout << " Executable: " << launch_at_login.GetExecutablePath() << "\n"; + auto args = launch_at_login.GetArguments(); + std::cout << " Arguments: "; + for (const auto& arg : args) { + std::cout << arg << " "; + } + std::cout << "\n\n"; + + // Check current state before enabling + std::cout << "Is enabled (before Enable): " << (launch_at_login.IsEnabled() ? "yes" : "no") + << "\n"; + + // Enable launch-at-login + std::cout << "Enabling launch-at-login...\n"; + if (launch_at_login.Enable()) { + std::cout << "Launch-at-login enabled successfully.\n"; + } else { + std::cout << "Failed to enable launch-at-login.\n"; + return 1; + } + + // Verify it is now enabled + std::cout << "Is enabled (after Enable): " << (launch_at_login.IsEnabled() ? "yes" : "no") + << "\n\n"; + + // Update the display name and re-enable to update the stored entry + launch_at_login.SetDisplayName("My Example App (Updated)"); + std::cout << "Updated display name to: " << launch_at_login.GetDisplayName() << "\n"; + launch_at_login.Enable(); + + // Disable launch-at-login + std::cout << "\nDisabling launch-at-login...\n"; + if (launch_at_login.Disable()) { + std::cout << "Launch-at-login disabled successfully.\n"; + } else { + std::cout << "Failed to disable launch-at-login.\n"; + return 1; + } + + // Verify it is now disabled + std::cout << "Is enabled (after Disable): " << (launch_at_login.IsEnabled() ? "yes" : "no") + << "\n\n"; + + std::cout << "Example completed successfully!\n\n"; + std::cout << "This example demonstrated:\n"; + std::cout << " * LaunchAtLogin::IsSupported() - Check platform support\n"; + std::cout << " * LaunchAtLogin(id, name) - Construct with identifier and display name\n"; + std::cout << " * LaunchAtLogin::GetId() - Get identifier\n"; + std::cout << " * LaunchAtLogin::GetDisplayName() - Get display name\n"; + std::cout << " * LaunchAtLogin::SetDisplayName() - Update display name\n"; + std::cout << " * LaunchAtLogin::SetProgram() - Set executable path and arguments\n"; + std::cout << " * LaunchAtLogin::GetExecutablePath() - Get configured executable\n"; + std::cout << " * LaunchAtLogin::GetArguments() - Get configured arguments\n"; + std::cout << " * LaunchAtLogin::Enable() - Register launch-at-login with the OS\n"; + std::cout << " * LaunchAtLogin::Disable() - Remove launch-at-login from the OS\n"; + std::cout << " * LaunchAtLogin::IsEnabled() - Query current registration state\n"; + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/menu_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/menu_c_example/CMakeLists.txt new file mode 100644 index 0000000..68e6c50 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/menu_c_example/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) +project(menu_c_example) + +set(CMAKE_C_STANDARD 11) + +# Include the nativeapi headers +include_directories(${CMAKE_SOURCE_DIR}/../../include) + +# Add the executable +add_executable(menu_c_example main.c) + +# Link against the nativeapi library +target_link_libraries(menu_c_example nativeapi) + +# Platform-specific settings +if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + # iOS uses UIKit, Foundation, CoreGraphics (already linked by nativeapi) +elseif(APPLE) + find_library(COCOA_LIBRARY Cocoa) + target_link_libraries(menu_c_example ${COCOA_LIBRARY}) +elseif(WIN32) + target_link_libraries(menu_c_example user32 kernel32) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTK3 REQUIRED gtk+-3.0) + target_include_directories(menu_c_example PRIVATE ${GTK3_INCLUDE_DIRS}) + target_link_libraries(menu_c_example ${GTK3_LIBRARIES}) +elseif(ANDROID) + target_link_libraries(menu_c_example log android) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/menu_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/menu_c_example/main.c new file mode 100644 index 0000000..1948a59 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/menu_c_example/main.c @@ -0,0 +1,221 @@ +#include +#include +#include +#include "../../src/capi/application_c.h" +#include "../../src/capi/menu_c.h" +#include "../../src/capi/positioning_strategy_c.h" + +// Event callback functions +// +// A menu or menu item emits one MenuEvent stream, tagged by concrete type, so +// a single listener covers clicks and submenu open/close alike. +void on_menu_event(const native_menu_event_t* event, void* user_data) { + const char* name = (const char*)user_data; + switch (event->type) { + case NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED: + printf("[EVENT] Menu item clicked: %s (ID: %u)\n", name, event->data.item_clicked.item_id); + break; + case NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_OPENED: + printf("[EVENT] Menu item submenu opened: %s (ID: %u)\n", name, + event->data.item_submenu_opened.item_id); + break; + case NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_CLOSED: + printf("[EVENT] Menu item submenu closed: %s (ID: %u)\n", name, + event->data.item_submenu_closed.item_id); + break; + case NATIVE_MENU_EVENT_TYPE_OPENED: + printf("[EVENT] Menu opened: %s (ID: %u)\n", name, event->data.opened.menu_id); + break; + case NATIVE_MENU_EVENT_TYPE_CLOSED: + printf("[EVENT] Menu closed: %s (ID: %u)\n", name, event->data.closed.menu_id); + break; + } +} + +int main() { + printf("=== Menu C API Event System Example ===\n"); + + // Create a menu + native_menu_t menu = native_menu_create(); + if (menu == NATIVE_INVALID_MENU) { + printf("Failed to create menu\n"); + return 1; + } + + printf("Created menu successfully\n"); + + // Create menu items + native_menu_item_t file_item = native_menu_item_create_with_label_and_type("New File", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_t checkbox_item = + native_menu_item_create_with_label_and_type("Word Wrap", NATIVE_MENU_ITEM_TYPE_CHECKBOX); + native_menu_item_t radio_item1 = + native_menu_item_create_with_label_and_type("View Mode 1", NATIVE_MENU_ITEM_TYPE_RADIO); + native_menu_item_t radio_item2 = + native_menu_item_create_with_label_and_type("View Mode 2", NATIVE_MENU_ITEM_TYPE_RADIO); + native_menu_item_t exit_item = native_menu_item_create_with_label_and_type("Exit", NATIVE_MENU_ITEM_TYPE_NORMAL); + + if (file_item == NATIVE_INVALID_MENU_ITEM || checkbox_item == NATIVE_INVALID_MENU_ITEM || + radio_item1 == NATIVE_INVALID_MENU_ITEM || radio_item2 == NATIVE_INVALID_MENU_ITEM || + exit_item == NATIVE_INVALID_MENU_ITEM) { + printf("Failed to create menu items\n"); + native_menu_free(menu); + return 1; + } + + // Set up radio group + native_menu_item_set_radio_group(radio_item1, 1); + native_menu_item_set_radio_group(radio_item2, 1); + native_menu_item_set_state(radio_item1, NATIVE_MENU_ITEM_STATE_CHECKED); + + // Set keyboard accelerators + native_keyboard_accelerator_t ctrl_n = {NATIVE_MODIFIER_KEY_CTRL, "N"}; + native_keyboard_accelerator_t ctrl_q = {NATIVE_MODIFIER_KEY_CTRL, "Q"}; + native_menu_item_set_accelerator(file_item, &ctrl_n); + native_menu_item_set_accelerator(exit_item, &ctrl_q); + + printf("Setting up event listeners using new event system...\n"); + + // Add event listeners using the new event system + native_listener_id_t file_listener = + native_menu_item_add_listener(file_item, on_menu_event, (void*)"New File"); + + native_listener_id_t checkbox_listener = + native_menu_item_add_listener(checkbox_item, on_menu_event, (void*)"Word Wrap"); + + native_listener_id_t exit_listener = + native_menu_item_add_listener(exit_item, on_menu_event, (void*)"Exit"); + + // Add menu event listeners + native_listener_id_t menu_listener = + native_menu_add_listener(menu, on_menu_event, (void*)"Main Menu"); + + // Check if listeners were added successfully + if (file_listener == NATIVE_INVALID_LISTENER_ID || + checkbox_listener == NATIVE_INVALID_LISTENER_ID || + exit_listener == NATIVE_INVALID_LISTENER_ID || menu_listener == NATIVE_INVALID_LISTENER_ID) { + printf("Failed to add some event listeners\n"); + } else { + printf("All event listeners added successfully\n"); + printf("Listener IDs: file=%llu, checkbox=%llu, exit=%llu, menu=%llu\n", + (unsigned long long)file_listener, (unsigned long long)checkbox_listener, + (unsigned long long)exit_listener, (unsigned long long)menu_listener); + } + + // Add items to menu + native_menu_add_item(menu, file_item); + native_menu_add_separator(menu); + native_menu_add_item(menu, checkbox_item); + native_menu_add_separator(menu); + native_menu_add_item(menu, radio_item1); + native_menu_add_item(menu, radio_item2); + native_menu_add_separator(menu); + native_menu_add_item(menu, exit_item); + + printf("Menu created with %lu items\n", native_menu_get_item_count(menu)); + + // Add submenu to demonstrate submenu events + native_menu_t submenu = native_menu_create(); + native_menu_item_t submenu_item1 = + native_menu_item_create_with_label_and_type("Submenu Item 1", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_t submenu_item2 = + native_menu_item_create_with_label_and_type("Submenu Item 2", NATIVE_MENU_ITEM_TYPE_NORMAL); + + native_menu_add_item(submenu, submenu_item1); + native_menu_add_item(submenu, submenu_item2); + + native_menu_item_t submenu_parent = + native_menu_item_create_with_label_and_type("Tools", NATIVE_MENU_ITEM_TYPE_SUBMENU); + native_menu_item_set_submenu(submenu_parent, submenu); + native_menu_add_item(menu, submenu_parent); + + // Add submenu event listener + native_menu_item_add_listener(submenu_parent, on_menu_event, (void*)"Tools"); + + printf("Added submenu with %lu items\n", native_menu_get_item_count(submenu)); + + // Note: Programmatic event triggering is no longer available via trigger API. + // Events can only be triggered through actual user interaction. + printf("\n=== Programmatic Event Triggering Removed ===\n"); + printf( + "Note: The trigger API has been removed. Events are now only " + "triggered through user interaction.\n"); + + // Demonstrate listener removal + printf("\n=== Testing Listener Removal ===\n"); + + printf("Removing checkbox click listener...\n"); + if (native_menu_item_remove_listener(checkbox_item, checkbox_listener)) { + printf("Checkbox click listener removed successfully\n"); + } else { + printf("Failed to remove checkbox click listener\n"); + } + + printf( + "Checkbox item listener removed. Events will now only be triggered " + "through user interaction.\n"); + + // Open menu as context menu (this may not work in console applications) + printf("\n=== Attempting to Open Context Menu ===\n"); + printf("Note: Context menu display may not work in console applications\n"); + + native_point_t point = {100, 100}; + native_positioning_strategy_t strategy = native_positioning_strategy_absolute(point); + if (native_menu_open(menu, strategy, NATIVE_PLACEMENT_BOTTOM_START)) { + printf("Context menu opened successfully (BOTTOM_START placement)\n"); + } else { + printf("Failed to open context menu (expected in console app)\n"); + } + native_positioning_strategy_free(strategy); + + // Test additional functionality + printf("\n=== Testing Additional Functionality ===\n"); + + native_menu_item_t additional_item = + native_menu_item_create_with_label_and_type("Additional Test", NATIVE_MENU_ITEM_TYPE_NORMAL); + + // Test that we can add multiple listeners for the same event + native_listener_id_t additional_listener1 = + native_menu_item_add_listener(additional_item, on_menu_event, (void*)"Additional Test 1"); + native_listener_id_t additional_listener2 = + native_menu_item_add_listener(additional_item, on_menu_event, (void*)"Additional Test 2"); + (void)additional_listener2; + + printf("Added multiple listeners for the same event\n"); + printf("Multiple listeners can be registered for the same event type.\n"); + + // Remove one listener + native_menu_item_remove_listener(additional_item, additional_listener1); + printf("Removed first listener. Remaining listener will receive events.\n"); + + native_menu_item_free(additional_item); + + printf("\n=== Event System Demo Complete ===\n"); + printf("This example demonstrates:\n"); + printf("1. Creating menus and menu items with different types\n"); + printf( + "2. Using the new event listener API with " + "native_menu_item_add_listener()\n"); + printf("3. Handling NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED\n"); + printf("4. Handling NATIVE_MENU_EVENT_TYPE_OPENED and NATIVE_MENU_EVENT_TYPE_CLOSED\n"); + printf( + "5. Handling NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_OPENED and " + "NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_CLOSED\n"); + printf("6. Event listener removal with native_menu_item_remove_listener()\n"); + printf("7. Multiple listeners for the same event type\n"); + printf("8. Manual state management for checkbox and radio items\n"); + printf("9. Submenu support with event handling\n"); + + // Cleanup + native_menu_item_free(file_item); + native_menu_item_free(checkbox_item); + native_menu_item_free(radio_item1); + native_menu_item_free(radio_item2); + native_menu_item_free(exit_item); + native_menu_item_free(submenu_item1); + native_menu_item_free(submenu_item2); + native_menu_item_free(submenu_parent); + native_menu_free(submenu); + native_menu_free(menu); + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/menu_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/menu_example/CMakeLists.txt new file mode 100644 index 0000000..5e8c529 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/menu_example/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) +project(menu_example) + +set(CMAKE_CXX_STANDARD 17) + +# Include the nativeapi headers +include_directories(${CMAKE_SOURCE_DIR}/../../include) + +# Add the executable +add_executable(menu_example main.cpp) + +# Link against the nativeapi library +target_link_libraries(menu_example nativeapi) + +# Platform-specific settings +if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + # iOS uses UIKit, Foundation, CoreGraphics (already linked by nativeapi) +elseif(APPLE) + find_library(COCOA_LIBRARY Cocoa) + target_link_libraries(menu_example ${COCOA_LIBRARY}) +elseif(WIN32) + target_link_libraries(menu_example user32 kernel32) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTK3 REQUIRED gtk+-3.0) + target_include_directories(menu_example PRIVATE ${GTK3_INCLUDE_DIRS}) + target_link_libraries(menu_example ${GTK3_LIBRARIES}) +elseif(ANDROID) + target_link_libraries(menu_example log android) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/menu_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/menu_example/main.cpp new file mode 100644 index 0000000..90e37c4 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/menu_example/main.cpp @@ -0,0 +1,167 @@ +#include +#include +#include +#include +#include "nativeapi.h" + +using namespace nativeapi; + +int main() { + std::cout << "=== Menu Event System Example ===" << std::endl; + + // Get the Application instance to initialize platform + Application& app = Application::GetInstance(); + + try { + // Create a menu + auto menu = std::make_shared(); + std::cout << "Created menu with ID: " << menu->GetId() << std::endl; + + // Create menu items with different types + auto file_item = std::make_shared("New File", MenuItemType::Normal); + auto separator_item = std::make_shared("", MenuItemType::Separator); + auto checkbox_item = std::make_shared("Word Wrap", MenuItemType::Checkbox); + auto radio_item1 = std::make_shared("View Mode 1", MenuItemType::Radio); + auto radio_item2 = std::make_shared("View Mode 2", MenuItemType::Radio); + auto exit_item = std::make_shared("Exit", MenuItemType::Normal); + + // Set up radio group + radio_item1->SetRadioGroup(1); + radio_item2->SetRadioGroup(1); + radio_item1->SetState(MenuItemState::Checked); + + // Set keyboard accelerators + file_item->SetAccelerator(KeyboardAccelerator("N", ModifierKey::Ctrl)); + exit_item->SetAccelerator(KeyboardAccelerator("Q", ModifierKey::Ctrl)); + + // Add event listeners using the new event system + std::cout << "Setting up event listeners..." << std::endl; + + // Listen to menu item selection events + file_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Menu item clicked: New File" + << " (ID: " << event.GetItemId() << ")" << std::endl; + }); + + checkbox_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Checkbox item clicked: Word Wrap" + << " (ID: " << event.GetItemId() << ") - Handle state manually" << std::endl; + }); + + // Note: State management is now handled by the application + + radio_item1->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Radio item 1 clicked: ID " << event.GetItemId() + << " - Handle state manually" << std::endl; + }); + + radio_item2->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Radio item 2 clicked: ID " << event.GetItemId() + << " - Handle state manually" << std::endl; + }); + + exit_item->AddListener([&app](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Exit item clicked: Exit" << std::endl; + std::cout << "Application exiting..." << std::endl; + app.Quit(0); + }); + + // Listen to menu events + menu->AddListener([](const MenuOpenedEvent& event) { + std::cout << "[EVENT] Menu opened: ID " << event.GetMenuId() << std::endl; + }); + + menu->AddListener([](const MenuClosedEvent& event) { + std::cout << "[EVENT] Menu closed: ID " << event.GetMenuId() << std::endl; + }); + + // Add items to menu + menu->AddItem(file_item); + menu->AddItem(separator_item); + menu->AddItem(checkbox_item); + menu->AddSeparator(); + menu->AddItem(radio_item1); + menu->AddItem(radio_item2); + menu->AddSeparator(); + menu->AddItem(exit_item); + + std::cout << "Menu created with " << menu->GetItemCount() << " items" << std::endl; + + // Demonstrate submenu + std::cout << "\n=== Testing Submenu ===" << std::endl; + auto submenu = std::make_shared(); + auto submenu_item1 = std::make_shared("Submenu Item 1", MenuItemType::Normal); + auto submenu_item2 = std::make_shared("Submenu Item 2", MenuItemType::Normal); + + submenu_item1->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Submenu item clicked: Submenu Item 1" << std::endl; + }); + + submenu_item2->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "[EVENT] Submenu item clicked: Submenu Item 2" << std::endl; + }); + + submenu->AddItem(submenu_item1); + submenu->AddItem(submenu_item2); + + auto submenu_parent = std::make_shared("Tools", MenuItemType::Submenu); + submenu_parent->SetSubmenu(submenu); + + // Add submenu event listeners + submenu_parent->AddListener( + [](const MenuItemSubmenuOpenedEvent& event) { + std::cout << "[EVENT] Submenu opened: ID " << event.GetItemId() << std::endl; + }); + + submenu_parent->AddListener( + [](const MenuItemSubmenuClosedEvent& event) { + std::cout << "[EVENT] Submenu closed: ID " << event.GetItemId() << std::endl; + }); + + menu->AddItem(submenu_parent); + + std::cout << "Added submenu with " << submenu->GetItemCount() << " items" << std::endl; + + std::cout << "\n=== Event System Demo Complete ===" << std::endl; + std::cout << "This example demonstrates:" << std::endl; + std::cout << "1. Creating menus and menu items with different types" << std::endl; + std::cout << "2. Using the new event system with AddListener()" << std::endl; + std::cout << "3. Handling MenuItemClickedEvent (state managed by application)" << std::endl; + std::cout << "4. Handling MenuOpenedEvent and MenuClosedEvent" << std::endl; + std::cout << "5. Handling MenuItemSubmenuOpenedEvent and " + "MenuItemSubmenuClosedEvent" + << std::endl; + std::cout << "6. Programmatic event emission using Emit()" << std::endl; + std::cout << "7. Submenu support with event propagation" << std::endl; + + std::cout << "\n========================================" << std::endl; + std::cout << "Starting application event loop..." << std::endl; + std::cout << "The menu will open shortly." << std::endl; + std::cout << "Click the Exit menu item to quit the application." << std::endl; + std::cout << "========================================" << std::endl; + + // Set up application started listener to open menu after event loop starts + app.AddListener([menu](const ApplicationStartedEvent& event) { + std::cout << "Application started - opening menu at (100, 100)" << std::endl; + + // Open menu as context menu at screen coordinates (100, 100) + if (menu->Open(PositioningStrategy::Absolute({100, 100}))) { + std::cout << "Context menu opened successfully!" << std::endl; + } else { + std::cout << "Failed to open context menu" << std::endl; + } + }); + + // Run the application event loop - this will block until app.Quit() is called + int exit_code = app.Run(); + + std::cout << "Exiting Menu Example..." << std::endl; + return exit_code; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/CMakeLists.txt new file mode 100644 index 0000000..ae34f2d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.10) + +project(message_dialog_c_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(message_dialog_c_example + "main.c" +) + +# Link main library +target_link_libraries(message_dialog_c_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(message_dialog_c_example PROPERTIES + OUTPUT_NAME "message_dialog_c_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.c" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() + diff --git a/packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/main.c new file mode 100644 index 0000000..3b976ab --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/message_dialog_c_example/main.c @@ -0,0 +1,148 @@ +#include +#include +#include + +#include "../../src/capi/message_dialog_c.h" +#include "../../src/capi/string_utils_c.h" + +int main() { + printf("=== MessageDialog C API Example ===\n\n"); + + // Example 1: Create a simple informational dialog + printf("1. Creating a simple informational dialog...\n"); + native_message_dialog_t info_dialog = native_message_dialog_create( + "Information", + "This is an informational message dialog.\n\n" + "MessageDialog can be used to display various types of messages to users."); + + if (info_dialog == NATIVE_INVALID_MESSAGE_DIALOG) { + fprintf(stderr, "Failed to create informational dialog\n"); + return 1; + } + + // Get and print dialog properties + char* title = native_message_dialog_get_title(info_dialog); + char* message = native_message_dialog_get_message(info_dialog); + if (title) { + printf("Dialog title: %s\n", title); + free_c_str(title); + } + if (message) { + printf("Dialog message: %s\n", message); + free_c_str(message); + } + + // Set modality to Application modal + native_message_dialog_set_modality(info_dialog, NATIVE_DIALOG_MODALITY_APPLICATION); + native_dialog_modality_t modality = native_message_dialog_get_modality(info_dialog); + printf("Modality: %s\n", + modality == NATIVE_DIALOG_MODALITY_APPLICATION ? "Application" : "Unknown"); + + printf("Opening dialog...\n"); + if (native_message_dialog_open(info_dialog)) { + printf("Dialog was opened successfully\n\n"); + } else { + printf("Failed to open dialog\n\n"); + } + + // Clean up first dialog + native_message_dialog_free(info_dialog); + + // Example 2: Create a warning dialog + printf("2. Creating a warning dialog...\n"); + native_message_dialog_t warning_dialog = native_message_dialog_create( + "Warning", + "This is a warning message.\n\n" + "Warning dialogs are used to alert users about potential issues."); + + if (warning_dialog != NATIVE_INVALID_MESSAGE_DIALOG) { + native_message_dialog_set_modality(warning_dialog, NATIVE_DIALOG_MODALITY_APPLICATION); + native_message_dialog_open(warning_dialog); + native_message_dialog_free(warning_dialog); + } + printf("\n"); + + // Example 3: Create a dialog with updated content + printf("3. Creating a dialog with updated content...\n"); + native_message_dialog_t dynamic_dialog = + native_message_dialog_create("Update Available", "Initial message"); + + if (dynamic_dialog != NATIVE_INVALID_MESSAGE_DIALOG) { + // Update the title and message before opening + native_message_dialog_set_title(dynamic_dialog, "System Update"); + native_message_dialog_set_message(dynamic_dialog, + "A new version of the application is available.\n\n" + "Version 2.0 includes:\n" + "• Improved performance\n" + "• New features\n" + "• Bug fixes\n\n" + "Would you like to update now?"); + + title = native_message_dialog_get_title(dynamic_dialog); + if (title) { + printf("Updated title: %s\n", title); + free_c_str(title); + } + + native_message_dialog_set_modality(dynamic_dialog, NATIVE_DIALOG_MODALITY_APPLICATION); + printf("Opening updated dialog...\n"); + native_message_dialog_open(dynamic_dialog); + native_message_dialog_free(dynamic_dialog); + } + printf("\n"); + + // Example 4: Demonstrate different modality types + printf("4. Demonstrating different modality types...\n"); + + // Non-modal dialog + native_message_dialog_t non_modal_dialog = native_message_dialog_create( + "Non-Modal Dialog", + "This dialog does not block interaction.\n\n" + "The application continues running and users can interact with other windows."); + if (non_modal_dialog != NATIVE_INVALID_MESSAGE_DIALOG) { + native_message_dialog_set_modality(non_modal_dialog, NATIVE_DIALOG_MODALITY_NONE); + modality = native_message_dialog_get_modality(non_modal_dialog); + printf("Modality: %s (non-modal)\n", + modality == NATIVE_DIALOG_MODALITY_NONE ? "None" : "Unknown"); + native_message_dialog_open(non_modal_dialog); + native_message_dialog_free(non_modal_dialog); + } + + // Application modal dialog + native_message_dialog_t app_modal_dialog = native_message_dialog_create( + "Application Modal", + "This dialog blocks interaction with all windows in the current application."); + if (app_modal_dialog != NATIVE_INVALID_MESSAGE_DIALOG) { + native_message_dialog_set_modality(app_modal_dialog, NATIVE_DIALOG_MODALITY_APPLICATION); + modality = native_message_dialog_get_modality(app_modal_dialog); + printf("Modality: %s\n", + modality == NATIVE_DIALOG_MODALITY_APPLICATION ? "Application" : "Unknown"); + native_message_dialog_open(app_modal_dialog); + native_message_dialog_free(app_modal_dialog); + } + + // Window modal dialog (behaves as Application on macOS) + native_message_dialog_t window_modal_dialog = native_message_dialog_create( + "Window Modal", + "This dialog blocks interaction with a specific parent window.\n\n" + "Note: On macOS, this behaves as Application."); + if (window_modal_dialog != NATIVE_INVALID_MESSAGE_DIALOG) { + native_message_dialog_set_modality(window_modal_dialog, NATIVE_DIALOG_MODALITY_WINDOW); + modality = native_message_dialog_get_modality(window_modal_dialog); + printf("Modality: %s\n", modality == NATIVE_DIALOG_MODALITY_WINDOW ? "Window" : "Unknown"); + native_message_dialog_open(window_modal_dialog); + native_message_dialog_free(window_modal_dialog); + } + + printf("\n=== MessageDialog C API Example Complete ===\n"); + printf("This example demonstrated:\n"); + printf("1. Creating MessageDialog instances\n"); + printf("2. Setting and getting title and message\n"); + printf("3. Updating dialog content programmatically\n"); + printf("4. Opening dialogs with different modality types (None, Application, Window)\n"); + printf("5. Modal vs non-modal dialog behavior\n"); + printf("6. Proper cleanup of dialog resources\n"); + + printf("\nExiting MessageDialog C API Example...\n"); + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/message_dialog_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/message_dialog_example/CMakeLists.txt new file mode 100644 index 0000000..da9ac5a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/message_dialog_example/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.10) + +project(message_dialog_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(message_dialog_example + "main.cpp" +) + +# Link main library +target_link_libraries(message_dialog_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(message_dialog_example PROPERTIES + OUTPUT_NAME "message_dialog_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() + diff --git a/packages/cnativeapi/cxx_impl/examples/message_dialog_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/message_dialog_example/main.cpp new file mode 100644 index 0000000..e76e43f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/message_dialog_example/main.cpp @@ -0,0 +1,111 @@ +#include +#include +#include "nativeapi.h" + +using namespace nativeapi; + +int main() { + std::cout << "=== MessageDialog Example ===" << std::endl; + + // Get the Application instance to initialize platform + Application& app = Application::GetInstance(); + + try { + std::cout << "\n1. Creating a simple informational dialog..." << std::endl; + + // Create a simple informational message dialog + auto info_dialog = std::make_shared( + "Information", + "This is an informational message dialog.\n\n" + "MessageDialog can be used to display various types of messages to users."); + + info_dialog->SetModality(DialogModality::Application); + + std::cout << "Dialog title: " << info_dialog->GetTitle() << std::endl; + std::cout << "Dialog message: " << info_dialog->GetMessage() << std::endl; + std::cout << "Opening dialog..." << std::endl; + + // Open the dialog (this will block until user dismisses it) + if (info_dialog->Open()) { + std::cout << "Dialog was opened successfully" << std::endl; + } else { + std::cout << "Failed to open dialog" << std::endl; + } + + std::cout << "\n2. Creating a warning dialog..." << std::endl; + + // Create a warning dialog + auto warning_dialog = std::make_shared( + "Warning", + "This is a warning message.\n\n" + "Warning dialogs are used to alert users about potential issues."); + + warning_dialog->SetModality(DialogModality::Application); + warning_dialog->Open(); + + std::cout << "\n3. Creating a dialog with updated content..." << std::endl; + + // Create a dialog and update its content + auto dynamic_dialog = std::make_shared("Update Available", "Initial message"); + + // Update the title and message before opening + dynamic_dialog->SetTitle("System Update"); + dynamic_dialog->SetMessage( + "A new version of the application is available.\n\n" + "Version 2.0 includes:\n" + "• Improved performance\n" + "• New features\n" + "• Bug fixes\n\n" + "Would you like to update now?"); + + dynamic_dialog->SetModality(DialogModality::Application); + + std::cout << "Updated title: " << dynamic_dialog->GetTitle() << std::endl; + std::cout << "Opening updated dialog..." << std::endl; + dynamic_dialog->Open(); + + std::cout << "\n4. Demonstrating different modality types..." << std::endl; + + // Non-modal dialog (default) + auto non_modal_dialog = std::make_shared( + "Non-Modal Dialog", + "This dialog does not block interaction.\n\n" + "The application continues running and users can interact with other windows."); + non_modal_dialog->SetModality(DialogModality::None); + std::cout << "Modality: None (non-modal)" << std::endl; + non_modal_dialog->Open(); + + // Application modal dialog + auto app_modal_dialog = std::make_shared( + "Application Modal", + "This dialog blocks interaction with all windows in the current application."); + app_modal_dialog->SetModality(DialogModality::Application); + std::cout << "Modality: Application" << std::endl; + app_modal_dialog->Open(); + + // Window modal dialog (behaves as Application on macOS) + auto window_modal_dialog = std::make_shared( + "Window Modal", + "This dialog blocks interaction with a specific parent window.\n\n" + "Note: On macOS, this behaves as Application."); + window_modal_dialog->SetModality(DialogModality::Window); + std::cout << "Modality: Window" << std::endl; + window_modal_dialog->Open(); + + std::cout << "\n=== MessageDialog Example Complete ===" << std::endl; + std::cout << "This example demonstrated:" << std::endl; + std::cout << "1. Creating MessageDialog instances" << std::endl; + std::cout << "2. Setting title and message" << std::endl; + std::cout << "3. Updating dialog content programmatically" << std::endl; + std::cout << "4. Opening dialogs with different modality types (None, Application, Window)" + << std::endl; + std::cout << "5. Modal vs non-modal dialog behavior" << std::endl; + + std::cout << "\nExiting MessageDialog Example..." << std::endl; + return 0; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } +} diff --git a/packages/cnativeapi/cxx_impl/examples/shortcut_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/shortcut_c_example/CMakeLists.txt new file mode 100644 index 0000000..54036eb --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/shortcut_c_example/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.15) +project(shortcut_c_example) + +set(CMAKE_C_STANDARD 11) + +add_executable(shortcut_c_example main.c) + +target_include_directories(shortcut_c_example PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/src/capi +) + +target_link_libraries(shortcut_c_example PRIVATE nativeapi) + diff --git a/packages/cnativeapi/cxx_impl/examples/shortcut_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/shortcut_c_example/main.c new file mode 100644 index 0000000..7565efb --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/shortcut_c_example/main.c @@ -0,0 +1,232 @@ +#include +#include +#include + +#ifdef _WIN32 +#include +#define SLEEP_MS(ms) Sleep(ms) +#else +#include +#define SLEEP_MS(ms) usleep((ms) * 1000) +#endif + +// Shortcut callback function +void on_shortcut_activated(void* user_data) { + const char* name = (const char*)user_data; + printf("Shortcut activated: %s\n", name); +} + +// Event callback function +void on_shortcut_event(const native_shortcut_event_t* event, void* user_data) { + (void)user_data; + const char* event_type_name = ""; + + switch (event->type) { + case NATIVE_SHORTCUT_EVENT_TYPE_ACTIVATED: + event_type_name = "ACTIVATED"; + break; + case NATIVE_SHORTCUT_EVENT_TYPE_REGISTERED: + event_type_name = "REGISTERED"; + break; + case NATIVE_SHORTCUT_EVENT_TYPE_UNREGISTERED: + event_type_name = "UNREGISTERED"; + break; + case NATIVE_SHORTCUT_EVENT_TYPE_REGISTRATION_FAILED: + event_type_name = "REGISTRATION_FAILED"; + if (event->data.registration_failed.error_message) { + printf("Shortcut event: %s - %s (ID: %u) - Error: %s\n", event_type_name, + event->accelerator, event->shortcut_id, + event->data.registration_failed.error_message); + return; + } + break; + } + + printf("Shortcut event: %s - %s (ID: %u)\n", event_type_name, event->accelerator, + event->shortcut_id); +} + +int main(void) { + printf("Shortcut Manager C API Example\n"); + printf("==============================\n\n"); + + // Check if shortcuts are supported + if (!native_shortcut_manager_is_supported()) { + printf("Global shortcuts are not supported on this platform\n"); + return 1; + } + + printf("Global shortcuts are supported\n\n"); + + // Register event callback + native_listener_id_t event_listener = + native_shortcut_manager_add_listener(on_shortcut_event, NULL); + if (event_listener == NATIVE_INVALID_LISTENER_ID) { + printf("Failed to register event callback\n"); + return 1; + } + + printf("Event callback registered (ID: %llu)\n\n", (unsigned long long)event_listener); + + // Register shortcuts + printf("Registering shortcuts...\n"); + + // Simple registration + native_shortcut_t shortcut1 = native_shortcut_manager_register_with_accelerator_and_callback( + "Ctrl+Shift+A", on_shortcut_activated, (void*)"Shortcut 1"); + + if (shortcut1 == NATIVE_INVALID_SHORTCUT) { + printf("Failed to register shortcut 1\n"); + } else { + char* accel1 = native_shortcut_get_accelerator(shortcut1); + printf("Registered shortcut 1: %s\n", accel1); + free_c_str(accel1); + } + + // Registration with options + native_shortcut_options_t options = {0}; + options.accelerator = "Ctrl+Shift+B"; + options.description = "Test shortcut 2"; + options.scope = NATIVE_SHORTCUT_SCOPE_GLOBAL; + options.enabled = true; + options.callback = on_shortcut_activated; + options.callback_user_data = (void*)"Shortcut 2"; + + native_shortcut_t shortcut2 = native_shortcut_manager_register_with_options(options); + + if (shortcut2 == NATIVE_INVALID_SHORTCUT) { + printf("Failed to register shortcut 2\n"); + } else { + char* accel2 = native_shortcut_get_accelerator(shortcut2); + char* desc2 = native_shortcut_get_description(shortcut2); + printf("Registered shortcut 2: %s - %s\n", accel2, desc2); + free_c_str(accel2); + free_c_str(desc2); + } + + // Register application-local shortcut + options.accelerator = "Ctrl+Shift+C"; + options.description = "Application-local shortcut"; + options.scope = NATIVE_SHORTCUT_SCOPE_APPLICATION; + options.callback_user_data = (void*)"Shortcut 3"; + + native_shortcut_t shortcut3 = native_shortcut_manager_register_with_options(options); + + if (shortcut3 == NATIVE_INVALID_SHORTCUT) { + printf("Failed to register shortcut 3\n"); + } else { + char* accel3 = native_shortcut_get_accelerator(shortcut3); + printf("Registered shortcut 3: %s (scope: %s)\n", accel3, + native_shortcut_get_scope(shortcut3) == NATIVE_SHORTCUT_SCOPE_GLOBAL ? "Global" + : "Application"); + free_c_str(accel3); + } + + printf("\n"); + + // Get all shortcuts + native_shortcut_list_t all_shortcuts = native_shortcut_manager_get_all(); + printf("Total shortcuts registered: %ld\n", all_shortcuts.count); + + for (long i = 0; i < all_shortcuts.count; i++) { + native_shortcut_t shortcut = all_shortcuts.shortcuts[i]; + char* accel = native_shortcut_get_accelerator(shortcut); + printf(" - %s (ID: %u, enabled: %s)\n", accel, native_shortcut_get_id(shortcut), + native_shortcut_is_enabled(shortcut) ? "yes" : "no"); + free_c_str(accel); + } + + native_shortcut_list_free(&all_shortcuts); + + printf("\n"); + + // Get shortcuts by scope + native_shortcut_list_t global_shortcuts = + native_shortcut_manager_get_by_scope(NATIVE_SHORTCUT_SCOPE_GLOBAL); + printf("Global shortcuts: %ld\n", global_shortcuts.count); + native_shortcut_list_free(&global_shortcuts); + + native_shortcut_list_t app_shortcuts = + native_shortcut_manager_get_by_scope(NATIVE_SHORTCUT_SCOPE_APPLICATION); + printf("Application shortcuts: %ld\n", app_shortcuts.count); + native_shortcut_list_free(&app_shortcuts); + + printf("\n"); + + // Test shortcut operations + if (shortcut2 != NATIVE_INVALID_SHORTCUT) { + printf("Testing shortcut operations on shortcut 2...\n"); + + // Disable shortcut + printf("Disabling shortcut 2...\n"); + native_shortcut_set_enabled(shortcut2, false); + printf("Shortcut 2 enabled: %s\n", native_shortcut_is_enabled(shortcut2) ? "yes" : "no"); + + // Re-enable shortcut + printf("Re-enabling shortcut 2...\n"); + native_shortcut_set_enabled(shortcut2, true); + printf("Shortcut 2 enabled: %s\n", native_shortcut_is_enabled(shortcut2) ? "yes" : "no"); + + // Update description + native_shortcut_set_description(shortcut2, "Updated description"); + char* updated_desc = native_shortcut_get_description(shortcut2); + printf("Shortcut 2 description: %s\n", updated_desc); + free_c_str(updated_desc); + + printf("\n"); + } + + // Test accelerator validation + printf("Testing accelerator validation...\n"); + printf("Is 'Ctrl+Shift+D' valid? %s\n", + native_shortcut_manager_is_valid_accelerator("Ctrl+Shift+D") ? "yes" : "no"); + printf("Is 'InvalidKey' valid? %s\n", + native_shortcut_manager_is_valid_accelerator("InvalidKey") ? "yes" : "no"); + printf("Is 'Ctrl+Shift+A' available? %s\n", + native_shortcut_manager_is_available("Ctrl+Shift+A") ? "yes" : "no"); + printf("Is 'Ctrl+Shift+Z' available? %s\n", + native_shortcut_manager_is_available("Ctrl+Shift+Z") ? "yes" : "no"); + + printf("\n"); + + // Wait for shortcuts to be triggered + printf("Press the registered shortcuts to test them:\n"); + printf(" - Ctrl+Shift+A (Shortcut 1)\n"); + printf(" - Ctrl+Shift+B (Shortcut 2)\n"); + printf(" - Ctrl+Shift+C (Shortcut 3 - application-local)\n"); + printf("\nPress Ctrl+C to exit...\n\n"); + + // Keep the program running to receive shortcut events + for (int i = 0; i < 60; i++) { + SLEEP_MS(1000); + } + + // Cleanup + printf("\nCleaning up...\n"); + + // Unregister shortcuts + if (shortcut1 != NATIVE_INVALID_SHORTCUT) { + native_shortcut_id_t id = native_shortcut_get_id(shortcut1); + if (native_shortcut_manager_unregister_with_id(id)) { + printf("Unregistered shortcut 1\n"); + } + } + + if (shortcut2 != NATIVE_INVALID_SHORTCUT) { + if (native_shortcut_manager_unregister_with_accelerator("Ctrl+Shift+B")) { + printf("Unregistered shortcut 2\n"); + } + } + + // Unregister all remaining shortcuts + int count = native_shortcut_manager_unregister_all(); + printf("Unregistered %d remaining shortcuts\n", count); + + // Unregister event callback + if (native_shortcut_manager_remove_listener(event_listener)) { + printf("Event callback unregistered\n"); + } + + printf("\nDone!\n"); + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/shortcut_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/shortcut_example/CMakeLists.txt new file mode 100644 index 0000000..6ca2776 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/shortcut_example/CMakeLists.txt @@ -0,0 +1,20 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +project(shortcut_example) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add executable +add_executable(shortcut_example main.cpp) + +# Link with the native API library +target_link_libraries(shortcut_example nativeapi) + +# Set include directories +target_include_directories(shortcut_example PRIVATE ../../include) + diff --git a/packages/cnativeapi/cxx_impl/examples/shortcut_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/shortcut_example/main.cpp new file mode 100644 index 0000000..f481264 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/shortcut_example/main.cpp @@ -0,0 +1,236 @@ +#include +#include +#include +#include +#include + +#include "nativeapi.h" + +using namespace nativeapi; + +// Global flag for graceful shutdown +static bool g_running = true; + +// Signal handler for graceful shutdown +void signal_handler(int sig) { + std::cout << "\nReceived signal " << sig << ", shutting down...\n"; + g_running = false; +} + +int main() { + std::cout << "ShortcutManager Example\n"; + std::cout << "=======================\n\n"; + + // Set up signal handlers + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Get the ShortcutManager singleton instance + auto& manager = ShortcutManager::GetInstance(); + + // Check if global shortcuts are supported + if (!manager.IsSupported()) { + std::cout << "⚠️ Global shortcuts are not supported on this platform or configuration.\n"; + std::cout << "This may be due to:\n"; + std::cout << " - Missing accessibility permissions (macOS)\n"; + std::cout << " - No display server available (Linux)\n"; + std::cout << " - Platform limitations\n\n"; + std::cout << "The example will continue, but shortcuts won't be triggered.\n\n"; + } else { + std::cout << "✓ Global shortcuts are supported\n\n"; + } + + // Add event listener for shortcut activations + auto activation_listener = + manager.AddListener([](const ShortcutActivatedEvent& event) { + std::cout << "🔔 Shortcut activated: " << event.GetAccelerator() + << " (ID: " << event.GetShortcutId() << ")\n"; + }); + + // Add event listener for registration events + auto registration_listener = + manager.AddListener([](const ShortcutRegisteredEvent& event) { + std::cout << "✓ Shortcut registered: " << event.GetAccelerator() + << " (ID: " << event.GetShortcutId() << ")\n"; + }); + + // Add event listener for unregistration events + auto unregistration_listener = + manager.AddListener([](const ShortcutUnregisteredEvent& event) { + std::cout << "✗ Shortcut unregistered: " << event.GetAccelerator() + << " (ID: " << event.GetShortcutId() << ")\n"; + }); + + // Add event listener for registration failures + auto failure_listener = manager.AddListener( + [](const ShortcutRegistrationFailedEvent& event) { + std::cout << "❌ Failed to register shortcut: " << event.GetAccelerator() << " - " + << event.GetErrorMessage() << "\n"; + }); + + std::cout << "Event listeners registered\n\n"; + + // Register shortcuts with simple callback + std::cout << "Registering shortcuts...\n"; + + auto shortcut1 = + manager.Register("Ctrl+Shift+A", []() { std::cout << " → Action A triggered!\n"; }); + + auto shortcut2 = + manager.Register("Ctrl+Shift+B", []() { std::cout << " → Action B triggered!\n"; }); + + auto shortcut3 = + manager.Register("Ctrl+Shift+C", []() { std::cout << " → Action C triggered!\n"; }); + + // Register shortcut with detailed options + ShortcutOptions options; + options.accelerator = "Ctrl+Shift+Q"; + options.description = "Quick quit action"; + options.scope = ShortcutScope::Global; + options.callback = []() { + std::cout << " → Quick quit triggered! (This would normally quit the app)\n"; + }; + + auto shortcut4 = manager.Register(options); + + std::cout << "\n"; + + // Display registered shortcuts + auto all_shortcuts = manager.GetAll(); + std::cout << "Currently registered shortcuts (" << all_shortcuts.size() << "):\n"; + for (const auto& shortcut : all_shortcuts) { + std::cout << " • " << shortcut->GetAccelerator(); + if (!shortcut->GetDescription().empty()) { + std::cout << " - " << shortcut->GetDescription(); + } + std::cout << " (ID: " << shortcut->GetId() << ", Scope: " + << (shortcut->GetScope() == ShortcutScope::Global ? "Global" : "Application") + << ", Enabled: " << (shortcut->IsEnabled() ? "Yes" : "No") << ")\n"; + } + std::cout << "\n"; + + // Demonstrate validation + std::cout << "Testing accelerator validation:\n"; + std::vector test_accelerators = { + "Ctrl+A", // Valid + "Ctrl+Shift+F1", // Valid + "Invalid", // Invalid + "Ctrl++", // Invalid + "Alt+Space", // Valid + }; + + for (const auto& acc : test_accelerators) { + bool valid = manager.IsValidAccelerator(acc); + bool available = manager.IsAvailable(acc); + std::cout << " • \"" << acc << "\" - Valid: " << (valid ? "Yes" : "No") + << ", Available: " << (available ? "Yes" : "No") << "\n"; + } + std::cout << "\n"; + + // Demonstrate enable/disable + std::cout << "Demonstrating enable/disable:\n"; + if (shortcut1) { + std::cout << " • Disabling shortcut: " << shortcut1->GetAccelerator() << "\n"; + shortcut1->SetEnabled(false); + std::cout << " Shortcut is now disabled. Pressing it won't trigger the callback.\n"; + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + std::cout << " • Re-enabling shortcut: " << shortcut1->GetAccelerator() << "\n"; + shortcut1->SetEnabled(true); + std::cout << " Shortcut is now enabled again.\n"; + } + std::cout << "\n"; + + // Demonstrate programmatic invocation + std::cout << "Demonstrating programmatic invocation:\n"; + if (shortcut2) { + std::cout << " • Manually invoking shortcut: " << shortcut2->GetAccelerator() << "\n"; + shortcut2->Invoke(); + } + std::cout << "\n"; + + // Demonstrate getting shortcuts by scope + std::cout << "Shortcuts by scope:\n"; + auto global_shortcuts = manager.GetByScope(ShortcutScope::Global); + auto app_shortcuts = manager.GetByScope(ShortcutScope::Application); + std::cout << " • Global shortcuts: " << global_shortcuts.size() << "\n"; + std::cout << " • Application shortcuts: " << app_shortcuts.size() << "\n"; + std::cout << "\n"; + + // Main loop + std::cout << "📋 Available shortcuts:\n"; + std::cout << " • Ctrl+Shift+A - Trigger Action A\n"; + std::cout << " • Ctrl+Shift+B - Trigger Action B\n"; + std::cout << " • Ctrl+Shift+C - Trigger Action C\n"; + std::cout << " • Ctrl+Shift+Q - Quick quit action\n"; + std::cout << "\n"; + std::cout << "Press the shortcuts above to see them in action.\n"; + std::cout << "Press Ctrl+C to exit.\n\n"; + + // Keep the main thread alive AND service the main-thread work queue. + // + // Events are delivered on the main thread, which means something has to run + // the platform main loop. A GUI app gets that from its UI framework; a console + // program like this one asks the library to do it. Plain sleeping here would + // keep the process alive but no event would ever arrive. + int seconds = 0; + while (g_running) { + if (!RunMainThreadLoopFor(1000)) { + // Platform without main-loop integration — fall back to sleeping so the + // example still terminates cleanly. + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + seconds++; + + // After 10 seconds, demonstrate unregistering a shortcut + if (seconds == 10 && shortcut3) { + std::cout << "\n⏰ 10 seconds elapsed. Unregistering shortcut: " + << shortcut3->GetAccelerator() << "\n\n"; + manager.Unregister(shortcut3->GetId()); + } + + // After 20 seconds, demonstrate disabling all shortcuts + if (seconds == 20) { + std::cout << "\n⏰ 20 seconds elapsed. Disabling all shortcut processing.\n"; + std::cout << "Shortcuts will remain registered but won't trigger.\n\n"; + manager.SetEnabled(false); + } + + // After 25 seconds, re-enable + if (seconds == 25) { + std::cout << "\n⏰ 25 seconds elapsed. Re-enabling shortcut processing.\n\n"; + manager.SetEnabled(true); + } + } + + // Cleanup + std::cout << "\nCleaning up...\n"; + + // Remove event listeners + manager.RemoveListener(activation_listener); + manager.RemoveListener(registration_listener); + manager.RemoveListener(unregistration_listener); + manager.RemoveListener(failure_listener); + + // Unregister all shortcuts + int count = manager.UnregisterAll(); + std::cout << "Unregistered " << count << " shortcuts\n"; + + std::cout << "\nExample completed successfully!\n"; + std::cout << "\nThis example demonstrated:\n"; + std::cout << " • ShortcutManager::GetInstance() - Get singleton instance\n"; + std::cout << " • ShortcutManager::IsSupported() - Check platform support\n"; + std::cout << " • ShortcutManager::Register() - Register shortcuts\n"; + std::cout << " • ShortcutManager::Unregister() - Unregister shortcuts\n"; + std::cout << " • ShortcutManager::GetAll() - Get all shortcuts\n"; + std::cout << " • ShortcutManager::GetByScope() - Filter by scope\n"; + std::cout << " • ShortcutManager::IsValidAccelerator() - Validate format\n"; + std::cout << " • ShortcutManager::IsAvailable() - Check availability\n"; + std::cout << " • ShortcutManager::SetEnabled() - Enable/disable processing\n"; + std::cout << " • Shortcut::SetEnabled() - Enable/disable individual shortcuts\n"; + std::cout << " • Shortcut::Invoke() - Programmatic invocation\n"; + std::cout << " • Event listeners for activation, registration, and errors\n"; + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/storage_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/storage_c_example/CMakeLists.txt new file mode 100644 index 0000000..420262b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/storage_c_example/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.10) + +project(storage_c_example) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Add executable +add_executable(storage_c_example main.c) + +# Link nativeapi library +target_link_libraries(storage_c_example PRIVATE nativeapi) + +# Platform-specific settings +if(ANDROID) + # Android platform doesn't need OpenSSL (secure_storage is stub implementation) +elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") + target_link_libraries(storage_c_example PRIVATE "-framework Security") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(PkgConfig REQUIRED) + pkg_check_modules(OPENSSL REQUIRED IMPORTED_TARGET openssl) + target_link_libraries(storage_c_example PRIVATE PkgConfig::OPENSSL pthread) +elseif(APPLE) + target_link_libraries(storage_c_example PRIVATE "-framework Cocoa" "-framework Security") +elseif(CMAKE_SYSTEM_NAME STREQUAL "OHOS") + # OHOS platform doesn't need OpenSSL (secure_storage is stub implementation) +elseif(WIN32) + target_link_libraries(storage_c_example PRIVATE user32 shell32 dwmapi gdiplus crypt32) +elseif(UNIX) + find_package(PkgConfig REQUIRED) + pkg_check_modules(OPENSSL REQUIRED IMPORTED_TARGET openssl) + target_link_libraries(storage_c_example PRIVATE PkgConfig::OPENSSL pthread) +endif() + diff --git a/packages/cnativeapi/cxx_impl/examples/storage_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/storage_c_example/main.c new file mode 100644 index 0000000..c7fceb0 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/storage_c_example/main.c @@ -0,0 +1,164 @@ +#include +#include +#include + +void demo_preferences() { + printf("=== Preferences C API Demo ===\n"); + + // Create preferences with custom scope + native_preferences_t prefs = native_preferences_create_with_scope("my_c_app"); + if (!prefs) { + printf("Failed to create preferences\n"); + return; + } + + // Store some values + native_preferences_set(prefs, "username", "alice"); + native_preferences_set(prefs, "theme", "light"); + native_preferences_set(prefs, "font_size", "12"); + + // Retrieve values + char* username = native_preferences_get(prefs, "username", ""); + char* theme = native_preferences_get(prefs, "theme", ""); + char* font_size = native_preferences_get(prefs, "font_size", ""); + + printf("Username: %s\n", username); + printf("Theme: %s\n", theme); + printf("Font size: %s\n", font_size); + + free_c_str(username); + free_c_str(theme); + free_c_str(font_size); + + // Check if key exists + if (native_preferences_contains(prefs, "language")) { + char* language = native_preferences_get(prefs, "language", ""); + printf("Language: %s\n", language); + free_c_str(language); + } else { + char* default_lang = native_preferences_get(prefs, "language", "en"); + printf("Language not set, using default: %s\n", default_lang); + free_c_str(default_lang); + } + + // Get all keys + native_string_list_t keys = native_preferences_get_keys(prefs); + printf("\nAll keys (%ld):\n", keys.count); + for (long i = 0; i < keys.count; i++) { + char* value = native_preferences_get(prefs, keys.items[i], ""); + printf(" - %s: %s\n", keys.items[i], value); + free_c_str(value); + } + native_string_list_free(&keys); + + // Get every entry in one call + native_string_map_t all = native_preferences_get_all(prefs); + printf("\nAll entries (%ld):\n", all.count); + for (long i = 0; i < all.count; i++) { + printf(" - %s = %s\n", all.keys[i], all.values[i]); + } + native_string_map_free(&all); + + // Get size + unsigned long size = native_preferences_get_size(prefs); + printf("Total items: %lu\n", size); + + // Remove a key + printf("\nRemoving 'font_size'...\n"); + native_preferences_remove(prefs, "font_size"); + printf("Size after removal: %lu\n", native_preferences_get_size(prefs)); + + // Get scope + char* scope = native_preferences_get_scope(prefs); + printf("Scope: %s\n", scope); + free_c_str(scope); + + // Clean up + native_preferences_free(prefs); + + printf("\n"); +} + +void demo_secure_storage() { + printf("=== Secure Storage C API Demo ===\n"); + + // Check if secure storage is available + if (!native_secure_storage_is_available()) { + printf("Secure storage is not available on this platform\n"); + return; + } + + // Create secure storage with custom scope + native_secure_storage_t storage = native_secure_storage_create_with_scope("my_c_app_secure"); + if (!storage) { + printf("Failed to create secure storage\n"); + return; + } + + // Store sensitive data + native_secure_storage_set(storage, "api_key", "sk-c-api-1234567890"); + native_secure_storage_set(storage, "secret", "my_secret_value"); + native_secure_storage_set(storage, "token", "bearer_token_xyz"); + + // Retrieve sensitive data + char* api_key = native_secure_storage_get(storage, "api_key", ""); + char* secret = native_secure_storage_get(storage, "secret", ""); + + printf("API Key: %s\n", api_key); + printf("Secret: %s\n", secret); + + free_c_str(api_key); + free_c_str(secret); + + // Get all keys + native_string_list_t keys = native_secure_storage_get_keys(storage); + printf("\nStored secure items (%ld):\n", keys.count); + for (long i = 0; i < keys.count; i++) { + printf(" - %s: [encrypted]\n", keys.items[i]); + } + native_string_list_free(&keys); + + // Check existence + if (native_secure_storage_contains(storage, "api_key")) { + printf("\nAPI key is securely stored\n"); + } + + // Get size + unsigned long size = native_secure_storage_get_size(storage); + printf("Total secure items: %lu\n", size); + + // Remove sensitive data + printf("\nRemoving 'token'...\n"); + native_secure_storage_remove(storage, "token"); + printf("Size after removal: %lu\n", native_secure_storage_get_size(storage)); + + // Get scope + char* storage_scope = native_secure_storage_get_scope(storage); + printf("Scope: %s\n", storage_scope); + free_c_str(storage_scope); + + // Clean up (optional: clear all for this demo) + // native_secure_storage_clear(storage); + + native_secure_storage_free(storage); + + printf("\n"); +} + +int main() { + printf("Storage C API Example\n"); + printf("=====================\n\n"); + + // Demo Preferences (plain text storage) + demo_preferences(); + + // Demo SecureStorage (encrypted storage) + demo_secure_storage(); + + printf("Done! Check your system's storage locations:\n"); + printf(" - macOS: NSUserDefaults & Keychain\n"); + printf(" - Windows: Registry & DPAPI\n"); + printf(" - Linux: ~/.config/nativeapi & ~/.local/share/nativeapi\n"); + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/storage_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/storage_example/CMakeLists.txt new file mode 100644 index 0000000..27a1995 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/storage_example/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.10) + +project(storage_example) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add executable +add_executable(storage_example main.cpp) + +# Link nativeapi library +target_link_libraries(storage_example PRIVATE nativeapi) + +# Platform-specific settings +if(ANDROID) + # Android platform doesn't need OpenSSL (secure_storage is stub implementation) +elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") + target_link_libraries(storage_example PRIVATE "-framework Security") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(PkgConfig REQUIRED) + pkg_check_modules(OPENSSL REQUIRED IMPORTED_TARGET openssl) + target_link_libraries(storage_example PRIVATE PkgConfig::OPENSSL pthread) +elseif(APPLE) + target_link_libraries(storage_example PRIVATE "-framework Cocoa" "-framework Security") +elseif(CMAKE_SYSTEM_NAME STREQUAL "OHOS") + # OHOS platform doesn't need OpenSSL (secure_storage is stub implementation) +elseif(WIN32) + target_link_libraries(storage_example PRIVATE user32 shell32 dwmapi gdiplus crypt32) +elseif(UNIX) + find_package(PkgConfig REQUIRED) + pkg_check_modules(OPENSSL REQUIRED IMPORTED_TARGET openssl) + target_link_libraries(storage_example PRIVATE PkgConfig::OPENSSL pthread) +endif() + diff --git a/packages/cnativeapi/cxx_impl/examples/storage_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/storage_example/main.cpp new file mode 100644 index 0000000..fea7964 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/storage_example/main.cpp @@ -0,0 +1,128 @@ +#include +#include + +using namespace nativeapi; + +void DemoPreferences() { + std::cout << "=== Preferences Demo ===" << std::endl; + + // Create preferences with custom scope + Preferences prefs("my_app"); + + // Store some values + prefs.Set("username", "john_doe"); + prefs.Set("theme", "dark"); + prefs.Set("font_size", "14"); + + // Retrieve values + std::cout << "Username: " << prefs.Get("username") << std::endl; + std::cout << "Theme: " << prefs.Get("theme") << std::endl; + std::cout << "Font size: " << prefs.Get("font_size") << std::endl; + + // Check if key exists + if (prefs.Contains("language")) { + std::cout << "Language: " << prefs.Get("language") << std::endl; + } else { + std::cout << "Language not set, using default: " << prefs.Get("language", "en") << std::endl; + } + + // Get all keys + auto keys = prefs.GetKeys(); + std::cout << "\nAll keys (" << prefs.GetSize() << "):" << std::endl; + for (const auto& key : keys) { + std::cout << " - " << key << ": " << prefs.Get(key) << std::endl; + } + + // Remove a key + std::cout << "\nRemoving 'font_size'..." << std::endl; + prefs.Remove("font_size"); + std::cout << "Size after removal: " << prefs.GetSize() << std::endl; + + std::cout << std::endl; +} + +void DemoSecureStorage() { + std::cout << "=== Secure Storage Demo ===" << std::endl; + + // Check if secure storage is available + if (!SecureStorage::IsAvailable()) { + std::cout << "Secure storage is not available on this platform" << std::endl; + return; + } + + // Create secure storage with custom scope + SecureStorage storage("my_app_secure"); + + // Store sensitive data + storage.Set("api_token", "sk-1234567890abcdef"); + storage.Set("encryption_key", "very_secret_key_12345"); + storage.Set("password", "super_secret_password"); + + // Retrieve sensitive data + std::cout << "API Token: " << storage.Get("api_token") << std::endl; + std::cout << "Password: " << storage.Get("password") << std::endl; + + // Get all keys (values are encrypted at rest) + auto keys = storage.GetKeys(); + std::cout << "\nStored secure items (" << storage.GetSize() << "):" << std::endl; + for (const auto& key : keys) { + std::cout << " - " << key << ": [encrypted]" << std::endl; + } + + // Check existence + if (storage.Contains("api_token")) { + std::cout << "\nAPI token is securely stored" << std::endl; + } + + // Remove sensitive data + std::cout << "\nRemoving 'password'..." << std::endl; + storage.Remove("password"); + std::cout << "Size after removal: " << storage.GetSize() << std::endl; + + // Clear all (for cleanup in this demo) + // storage.Clear(); + + std::cout << std::endl; +} + +void DemoStorageInterface() { + std::cout << "=== Storage Interface Demo ===" << std::endl; + + // Use Storage interface polymorphically + Storage* storage = new Preferences("polymorphic_test"); + + storage->Set("test_key", "test_value"); + std::cout << "Stored via Storage interface: " << storage->Get("test_key") << std::endl; + + delete storage; + + std::cout << std::endl; +} + +int main() { + std::cout << "Storage Example - Web Storage-like API for C++" << std::endl; + std::cout << "================================================" << std::endl; + std::cout << std::endl; + + try { + // Demo Preferences (plain text storage) + DemoPreferences(); + + // Demo SecureStorage (encrypted storage) + DemoSecureStorage(); + + // Demo polymorphic usage + DemoStorageInterface(); + + std::cout << "Done! Check your system's storage locations:" << std::endl; + std::cout << " - macOS: NSUserDefaults & Keychain" << std::endl; + std::cout << " - Windows: Registry & DPAPI" << std::endl; + std::cout << " - Linux: ~/.config/nativeapi & ~/.local/share/nativeapi" << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/CMakeLists.txt new file mode 100644 index 0000000..95fee5a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(tray_icon_c_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(tray_icon_c_example + "main.c" +) + +# Link main library +target_link_libraries(tray_icon_c_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(tray_icon_c_example PROPERTIES + OUTPUT_NAME "tray_icon_c_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/main.c new file mode 100644 index 0000000..a1d149b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/tray_icon_c_example/main.c @@ -0,0 +1,229 @@ +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +// Include individual C API headers instead of the full nativeapi.h +#include "../../src/capi/application_c.h" +#include "../../src/capi/menu_c.h" +#include "../../src/capi/string_utils_c.h" +#include "../../src/capi/tray_icon_c.h" +#include "../../src/capi/tray_manager_c.h" + +// Menu item IDs (stored globally for identification) +static native_menu_item_id_t exit_item_id = 0; +static native_menu_item_id_t show_message_item_id = 0; + +// Event callback functions +// +// Each emitter takes a single listener now; the event carries a tag saying +// which concrete event arrived. +void on_menu_event(const native_menu_event_t* event, void* user_data) { + (void)user_data; + switch (event->type) { + case NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED: { + native_menu_item_id_t item_id = event->data.item_clicked.item_id; + printf("Menu item clicked: ID=%u\n", item_id); + if (item_id == exit_item_id) { + printf("Exiting application...\n"); + exit(0); + } else if (item_id == show_message_item_id) { + printf("Hello from tray menu!\n"); + } + break; + } + case NATIVE_MENU_EVENT_TYPE_OPENED: + printf("Menu opened: ID=%u\n", event->data.opened.menu_id); + break; + case NATIVE_MENU_EVENT_TYPE_CLOSED: + printf("Menu closed: ID=%u\n", event->data.closed.menu_id); + break; + default: + break; + } +} + +void on_tray_event(const native_tray_icon_event_t* event, void* user_data) { + (void)user_data; + switch (event->type) { + case NATIVE_TRAY_ICON_EVENT_TYPE_CLICKED: + printf("Tray icon clicked! ID=%u\n", event->data.clicked.tray_icon_id); + break; + case NATIVE_TRAY_ICON_EVENT_TYPE_RIGHT_CLICKED: + printf("Tray icon right clicked! ID=%u\n", event->data.right_clicked.tray_icon_id); + break; + case NATIVE_TRAY_ICON_EVENT_TYPE_DOUBLE_CLICKED: + printf("Tray icon double clicked! ID=%u\n", event->data.double_clicked.tray_icon_id); + break; + } +} + +int main() { + printf("=== Tray Menu C API Example ===\n"); + + // Check if system tray is supported + if (!native_tray_manager_is_supported()) { + printf("Error: System tray is not supported on this platform!\n"); + return 1; + } + + printf("System tray is supported.\n"); + + // Create a menu + native_menu_t menu = native_menu_create(); + if (menu == NATIVE_INVALID_MENU) { + printf("Error: Failed to create menu!\n"); + return 1; + } + + printf("Created menu with ID: %u\n", native_menu_get_id(menu)); + + // Create menu items + native_menu_item_t item1 = native_menu_item_create_with_label_and_type("Show Message", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_t item2 = native_menu_item_create_with_label_and_type("Settings", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_t checkbox = + native_menu_item_create_with_label_and_type("Enable Notifications", NATIVE_MENU_ITEM_TYPE_CHECKBOX); + native_menu_item_t exit_item = native_menu_item_create_with_label_and_type("Exit", NATIVE_MENU_ITEM_TYPE_NORMAL); + + if (item1 == NATIVE_INVALID_MENU_ITEM || item2 == NATIVE_INVALID_MENU_ITEM || + checkbox == NATIVE_INVALID_MENU_ITEM || exit_item == NATIVE_INVALID_MENU_ITEM) { + printf("Error: Failed to create menu items!\n"); + native_menu_free(menu); + return 1; + } + + // Store menu item IDs for later identification + show_message_item_id = native_menu_item_get_id(item1); + exit_item_id = native_menu_item_get_id(exit_item); + + // Set up menu item properties + native_menu_item_set_enabled(item1, true); + native_menu_item_set_tooltip(item1, "Click to show a message"); + + // Set up keyboard accelerator for exit item + native_keyboard_accelerator_t exit_accel = {.modifiers = NATIVE_MODIFIER_KEY_CTRL, .key = "Q"}; + native_menu_item_set_accelerator(exit_item, &exit_accel); + + // Set checkbox state + native_menu_item_set_state(checkbox, NATIVE_MENU_ITEM_STATE_CHECKED); + + // Set up event listeners using new API + native_menu_item_add_listener(item1, on_menu_event, NULL); + native_menu_item_add_listener(item2, on_menu_event, NULL); + native_menu_item_add_listener(exit_item, on_menu_event, NULL); + native_menu_item_add_listener(checkbox, on_menu_event, NULL); + + // Add items to menu + native_menu_add_item(menu, item1); + native_menu_add_item(menu, item2); + native_menu_add_item(menu, checkbox); + native_menu_add_separator(menu); + native_menu_add_item(menu, exit_item); + + printf("Added %lu items to menu\n", native_menu_get_item_count(menu)); + + // Set menu event listeners using new API + native_menu_add_listener(menu, on_menu_event, NULL); + + // Create a submenu example + native_menu_t submenu = native_menu_create(); + if (submenu != NATIVE_INVALID_MENU) { + native_menu_item_t sub_item1 = + native_menu_item_create_with_label_and_type("Sub Item 1", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_t sub_item2 = + native_menu_item_create_with_label_and_type("Sub Item 2", NATIVE_MENU_ITEM_TYPE_NORMAL); + + if (sub_item1 != NATIVE_INVALID_MENU_ITEM && sub_item2 != NATIVE_INVALID_MENU_ITEM) { + native_menu_add_item(submenu, sub_item1); + native_menu_add_item(submenu, sub_item2); + + // Create submenu item and add to main menu + native_menu_item_t submenu_item = + native_menu_item_create_with_label_and_type("More Options", NATIVE_MENU_ITEM_TYPE_SUBMENU); + if (submenu_item != NATIVE_INVALID_MENU_ITEM) { + native_menu_item_set_submenu(submenu_item, submenu); + native_menu_add_item(menu, submenu_item); + printf("Created submenu with %lu items\n", native_menu_get_item_count(submenu)); + } + } + } + + // Create tray icon + native_tray_icon_t tray_icon = native_tray_icon_create(); + if (tray_icon == NATIVE_INVALID_TRAY_ICON) { + printf("Error: Failed to create tray icon!\n"); + native_menu_free(menu); + return 1; + } + + printf("Created tray icon with ID: %u\n", native_tray_icon_get_id(tray_icon)); + + // Set up tray icon properties + native_tray_icon_set_title(tray_icon, "My App"); + native_tray_icon_set_tooltip(tray_icon, "My Application - Right click for menu"); + + // Set the context menu + native_tray_icon_set_context_menu(tray_icon, menu); + + // Set context menu trigger to automatically show menu on right click + native_tray_icon_set_context_menu_trigger(tray_icon, NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED); + + // Get and display the current trigger mode + native_context_menu_trigger_t current_trigger = + native_tray_icon_get_context_menu_trigger(tray_icon); + printf("Context menu trigger mode: "); + switch (current_trigger) { + case NATIVE_CONTEXT_MENU_TRIGGER_NONE: + printf("None (manual control)\n"); + break; + case NATIVE_CONTEXT_MENU_TRIGGER_CLICKED: + printf("Left Click\n"); + break; + case NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED: + printf("Right Click\n"); + break; + case NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED: + printf("Double Click\n"); + break; + } + + // Set up tray icon event listeners using new API + native_tray_icon_add_listener(tray_icon, on_tray_event, NULL); + + // Show the tray icon + if (native_tray_icon_set_visible(tray_icon, true)) { + printf("Tray icon is now visible\n"); + } else { + printf("Warning: Failed to show tray icon\n"); + } + + // Get tray icon bounds + native_rectangle_t bounds = native_tray_icon_get_bounds(tray_icon); + printf("Tray icon bounds: x=%.1f, y=%.1f, width=%.1f, height=%.1f\n", bounds.x, bounds.y, + bounds.width, bounds.height); + + // Show all managed tray icons + native_tray_icon_list_t tray_list = native_tray_manager_get_all(); + printf("Total managed tray icons: %ld\n", tray_list.count); + native_tray_icon_list_free(&tray_list); + + printf("\n=== Tray icon and menu are now active ===\n"); + printf("- Click the tray icon to see click message\n"); + printf("- Right click the tray icon to auto-open context menu\n"); + printf("- Double click the tray icon to see double click message\n"); + printf("- Use menu items to interact with the application\n"); + printf("- Click 'Exit' to quit\n"); + printf("\nNote: Context menu automatically shows on right-click\n"); + printf(" because we set NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED.\n"); + printf("\nRunning... (Press Ctrl+C to force quit)\n"); + + // Run the application event loop + int exit_code = native_application_run(); + + return exit_code; +} diff --git a/packages/cnativeapi/cxx_impl/examples/tray_icon_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/tray_icon_example/CMakeLists.txt new file mode 100644 index 0000000..742b559 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/tray_icon_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(tray_icon_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(tray_icon_example + "main.cpp" +) + +# Link main library +target_link_libraries(tray_icon_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(tray_icon_example PROPERTIES + OUTPUT_NAME "tray_icon_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/tray_icon_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/tray_icon_example/main.cpp new file mode 100644 index 0000000..979fd62 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/tray_icon_example/main.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include + +#include "../../src/application.h" +#include "../../src/image.h" +#include "../../src/menu.h" +#include "../../src/tray_icon.h" +#include "../../src/tray_manager.h" + +using namespace nativeapi; +using nativeapi::Menu; +using nativeapi::MenuItem; +using nativeapi::MenuItemClickedEvent; +using nativeapi::MenuItemType; + +int main() { + std::cout << "Starting TrayIcon Example..." << std::endl; + + // Get the Application instance - this handles platform initialization + Application& app = Application::GetInstance(); + + // Check if tray icons are supported + TrayManager& trayManager = TrayManager::GetInstance(); + if (!trayManager.IsSupported()) { + std::cerr << "Tray icons are not supported on this platform!" << std::endl; + return 1; + } + + // Create a tray icon directly + auto trayIcon = std::make_shared(); + if (!trayIcon) { + std::cerr << "Failed to create tray icon!" << std::endl; + return 1; + } + + // Set up the tray icon + trayIcon->SetTitle("Test App"); + trayIcon->SetTooltip("This is a test tray icon"); + + // Set up event listeners + trayIcon->AddListener([](const TrayIconClickedEvent& event) { + std::cout << "*** TRAY ICON LEFT CLICKED! ***" << std::endl; + std::cout << "This is the left click handler working!" << std::endl; + std::cout << "Tray icon ID: " << event.GetTrayIconId() << std::endl; + }); + + trayIcon->AddListener([](const TrayIconRightClickedEvent& event) { + std::cout << "*** TRAY ICON RIGHT CLICKED! ***" << std::endl; + std::cout << "This is the right click handler working!" << std::endl; + std::cout << "Tray icon ID: " << event.GetTrayIconId() << std::endl; + // Note: Context menu will be auto-triggered by SetContextMenuTrigger below + }); + + trayIcon->AddListener([](const TrayIconDoubleClickedEvent& event) { + std::cout << "*** TRAY ICON DOUBLE CLICKED! ***" << std::endl; + std::cout << "This is the double click handler working!" << std::endl; + std::cout << "Tray icon ID: " << event.GetTrayIconId() << std::endl; + }); + + // Create context menu + auto context_menu = std::make_shared(); + + // Add menu items + auto status_item = std::make_shared("Status: Running", MenuItemType::Normal); + status_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "Status clicked from context menu" << std::endl; + }); + context_menu->AddItem(status_item); + + // Add separator + context_menu->AddSeparator(); + + // Add settings item + auto settings_item = std::make_shared("Settings...", MenuItemType::Normal); + settings_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "Settings clicked from context menu" << std::endl; + std::cout << "Opening settings dialog..." << std::endl; + }); + context_menu->AddItem(settings_item); + + // Add about item + auto about_item = std::make_shared("About", MenuItemType::Normal); + about_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "About clicked from context menu" << std::endl; + std::cout << "TrayIcon Example v1.0 - Native API Demo" << std::endl; + }); + context_menu->AddItem(about_item); + + // Add another separator + context_menu->AddSeparator(); + + // Add exit item + auto exit_item = std::make_shared("Exit", MenuItemType::Normal); + exit_item->AddListener([&app](const MenuItemClickedEvent& event) { + std::cout << "Exit clicked from context menu" << std::endl; + app.Quit(0); + }); + context_menu->AddItem(exit_item); + + // Set the context menu to the tray icon + trayIcon->SetContextMenu(context_menu); + + // Set context menu trigger to automatically show menu on right click + // This is the common behavior on Windows and most desktop environments + trayIcon->SetContextMenuTrigger(ContextMenuTrigger::RightClicked); + + // Get and display the current trigger mode + ContextMenuTrigger currentTrigger = trayIcon->GetContextMenuTrigger(); + std::cout << "Context menu trigger mode: "; + switch (currentTrigger) { + case ContextMenuTrigger::None: + std::cout << "None (manual control)"; + break; + case ContextMenuTrigger::Clicked: + std::cout << "Left Click"; + break; + case ContextMenuTrigger::RightClicked: + std::cout << "Right Click"; + break; + case ContextMenuTrigger::DoubleClicked: + std::cout << "Double Click"; + break; + } + std::cout << std::endl; + + // Show the tray icon + if (trayIcon->SetVisible(true)) { + std::cout << "Tray icon is now visible!" << std::endl; + } else { + std::cerr << "Failed to show tray icon!" << std::endl; + return 1; + } + + // Get and display bounds + Rectangle bounds = trayIcon->GetBounds(); + std::cout << "Tray icon bounds: x=" << bounds.x << ", y=" << bounds.y + << ", width=" << bounds.width << ", height=" << bounds.height << std::endl; + + std::cout << "========================================" << std::endl; + std::cout << "Tray icon example is now running!" << std::endl; + std::cout << "Try clicking on the tray icon:" << std::endl; + std::cout << "- Left click: Single click event" << std::endl; + std::cout << "- Right click: Auto-opens context menu (via SetContextMenuTrigger)" << std::endl; + std::cout << "- Double click: Quick double click event" << std::endl; + std::cout << "- Context menu: Right-click to see options including Exit" << std::endl; + std::cout << std::endl; + std::cout << "Note: The context menu is automatically shown on right-click" << std::endl; + std::cout << " because we set ContextMenuTrigger::RightClicked." << std::endl; + std::cout << " You can also use Clicked, DoubleClicked, or None for manual control." + << std::endl; + std::cout << std::endl; + std::cout << "Use the Exit menu item to quit the application." << std::endl; + std::cout << "========================================" << std::endl; + + // Set up application event listeners + app.AddListener([&trayIcon](const ApplicationExitingEvent& event) { + std::cout << "Application is exiting with code: " << event.GetExitCode() << std::endl; + // Hide the tray icon before exiting + if (trayIcon) { + trayIcon->SetVisible(false); + } + }); + + // Run the application event loop - this will block until app.Quit() is called + int exit_code = app.Run(); + + std::cout << "Exiting TrayIcon Example..." << std::endl; + return exit_code; +} diff --git a/packages/cnativeapi/cxx_impl/examples/url_opener_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/url_opener_c_example/CMakeLists.txt new file mode 100644 index 0000000..3f6bbe8 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/url_opener_c_example/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.10) + +project(url_opener_c_example) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_executable(url_opener_c_example main.c) + +target_link_libraries(url_opener_c_example nativeapi) + +target_include_directories(url_opener_c_example PRIVATE ../../include) diff --git a/packages/cnativeapi/cxx_impl/examples/url_opener_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/url_opener_c_example/main.c new file mode 100644 index 0000000..96d16f7 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/url_opener_c_example/main.c @@ -0,0 +1,27 @@ +#include +#include + +int main(void) { + printf("URL Opener C API Example\n"); + printf("========================\n\n"); + + if (!native_url_opener_is_supported()) { + printf("URL opening is not supported on this platform.\n"); + return 0; + } + + printf("URL opening is supported.\n"); + printf("Opening https://example.com ...\n"); + + native_url_open_result_t result = native_url_opener_open("https://example.com"); + if (result.success) { + printf("URL opened successfully.\n"); + } else { + fprintf(stderr, "Failed to open URL.\n"); + fprintf(stderr, "Error code: %d\n", (int)result.error_code); + fprintf(stderr, "Message: %s\n", result.error_message ? result.error_message : "(none)"); + } + + native_url_open_result_free(&result); + return result.success ? 0 : 1; +} diff --git a/packages/cnativeapi/cxx_impl/examples/window_c_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/window_c_example/CMakeLists.txt new file mode 100644 index 0000000..e5f0751 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/window_c_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(window_c_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(window_c_example + "main.c" +) + +# Link main library +target_link_libraries(window_c_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(window_c_example PROPERTIES + OUTPUT_NAME "window_c_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/window_c_example/main.c b/packages/cnativeapi/cxx_impl/examples/window_c_example/main.c new file mode 100644 index 0000000..1a7197a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/window_c_example/main.c @@ -0,0 +1,467 @@ +#include +#include +#include +#include + +// Include C API headers +#include "../../src/capi/accessibility_manager_c.h" +#include "../../src/capi/application_c.h" +#include "../../src/capi/image_c.h" +#include "../../src/capi/menu_c.h" +#include "../../src/capi/string_utils_c.h" +#include "../../src/capi/tray_icon_c.h" +#include "../../src/capi/tray_manager_c.h" +#include "../../src/capi/window_c.h" +#include "../../src/capi/window_manager_c.h" + +// Global variables to store handles +// Handles are opaque integers, not pointers — see docs/handle-ownership.md. +static native_window_t g_window = NATIVE_INVALID_WINDOW; +static native_tray_icon_t g_tray_icon = NATIVE_INVALID_TRAY_ICON; +static native_menu_t g_context_menu = NATIVE_INVALID_MENU; + +// Event callback functions +// +// Every emitter now takes a single listener and delivers a tagged event, so +// what used to be three registrations is one switch. +void on_tray_icon_event(const native_tray_icon_event_t* event, void* user_data) { + (void)user_data; + switch (event->type) { + case NATIVE_TRAY_ICON_EVENT_TYPE_CLICKED: + printf("*** TRAY ICON LEFT CLICKED! ***\n"); + printf("Tray icon ID: %u\n", event->data.clicked.tray_icon_id); + break; + case NATIVE_TRAY_ICON_EVENT_TYPE_RIGHT_CLICKED: + printf("*** TRAY ICON RIGHT CLICKED! ***\n"); + printf("Tray icon ID: %u\n", event->data.right_clicked.tray_icon_id); + break; + case NATIVE_TRAY_ICON_EVENT_TYPE_DOUBLE_CLICKED: + printf("*** TRAY ICON DOUBLE CLICKED! ***\n"); + printf("Tray icon ID: %u\n", event->data.double_clicked.tray_icon_id); + break; + } +} + +void on_show_window_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("Show Window clicked from context menu\n"); + if (g_window != NATIVE_INVALID_WINDOW) { + native_window_show(g_window); + native_window_focus(g_window); + } +} + +void on_hide_window_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("Hide Window clicked from context menu\n"); + if (g_window != NATIVE_INVALID_WINDOW) { + native_window_hide(g_window); + } +} + +void on_toggle_title_bar_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("Toggle Title Bar clicked from context menu\n"); + if (g_window != NATIVE_INVALID_WINDOW) { + native_title_bar_style_t current_style = native_window_get_title_bar_style(g_window); + native_title_bar_style_t new_style = (current_style == NATIVE_TITLE_BAR_STYLE_HIDDEN) + ? NATIVE_TITLE_BAR_STYLE_NORMAL + : NATIVE_TITLE_BAR_STYLE_HIDDEN; + native_window_set_title_bar_style(g_window, new_style); + printf("Title bar style changed to: %s\n", + (new_style == NATIVE_TITLE_BAR_STYLE_HIDDEN) ? "Hidden" : "Normal"); + } +} + +void on_about_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("About clicked from context menu\n"); + printf("Window Example v1.0 - Native API Demo\n"); +} + +void on_clear_cache_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("Clear Cache clicked from submenu\n"); +} + +void on_reset_settings_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("Reset Settings clicked from submenu\n"); +} + +void on_debug_mode_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t debug_mode_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_state_t current_state = native_menu_item_get_state(debug_mode_item); + native_menu_item_state_t new_state = (current_state == NATIVE_MENU_ITEM_STATE_CHECKED) + ? NATIVE_MENU_ITEM_STATE_UNCHECKED + : NATIVE_MENU_ITEM_STATE_CHECKED; + native_menu_item_set_state(debug_mode_item, new_state); + printf("Debug Mode %s\n", (new_state == NATIVE_MENU_ITEM_STATE_CHECKED) ? "enabled" : "disabled"); +} + +void on_auto_start_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t auto_start_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_state_t current_state = native_menu_item_get_state(auto_start_item); + native_menu_item_state_t new_state = (current_state == NATIVE_MENU_ITEM_STATE_CHECKED) + ? NATIVE_MENU_ITEM_STATE_UNCHECKED + : NATIVE_MENU_ITEM_STATE_CHECKED; + native_menu_item_set_state(auto_start_item, new_state); + printf("Auto Start %s\n", (new_state == NATIVE_MENU_ITEM_STATE_CHECKED) ? "enabled" : "disabled"); +} + +void on_notifications_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t notifications_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_state_t current_state = native_menu_item_get_state(notifications_item); + native_menu_item_state_t new_state = (current_state == NATIVE_MENU_ITEM_STATE_CHECKED) + ? NATIVE_MENU_ITEM_STATE_UNCHECKED + : NATIVE_MENU_ITEM_STATE_CHECKED; + native_menu_item_set_state(notifications_item, new_state); + printf("Notifications %s\n", + (new_state == NATIVE_MENU_ITEM_STATE_CHECKED) ? "enabled" : "disabled"); +} + +void on_sync_item_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t sync_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_state_t current_state = native_menu_item_get_state(sync_item); + native_menu_item_state_t next_state; + const char* state_name; + + // Cycle through states: Mixed -> Checked -> Unchecked -> Mixed + switch (current_state) { + case NATIVE_MENU_ITEM_STATE_MIXED: + next_state = NATIVE_MENU_ITEM_STATE_CHECKED; + state_name = "enabled"; + break; + case NATIVE_MENU_ITEM_STATE_CHECKED: + next_state = NATIVE_MENU_ITEM_STATE_UNCHECKED; + state_name = "disabled"; + break; + case NATIVE_MENU_ITEM_STATE_UNCHECKED: + default: + next_state = NATIVE_MENU_ITEM_STATE_MIXED; + state_name = "partial"; + break; + } + + native_menu_item_set_state(sync_item, next_state); + printf("Sync Status: %s\n", state_name); +} + +void on_light_theme_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t light_theme_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_set_state(light_theme_item, NATIVE_MENU_ITEM_STATE_CHECKED); + printf("Light theme selected\n"); +} + +void on_dark_theme_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t dark_theme_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_set_state(dark_theme_item, NATIVE_MENU_ITEM_STATE_CHECKED); + printf("Dark theme selected\n"); +} + +void on_auto_theme_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + native_menu_item_t auto_theme_item = (native_menu_item_t)(uintptr_t)user_data; + native_menu_item_set_state(auto_theme_item, NATIVE_MENU_ITEM_STATE_CHECKED); + printf("Auto theme selected\n"); +} + +void on_exit_clicked(const native_menu_event_t* event, void* user_data) { + if (event->type != NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED) { + return; + } + printf("Exit clicked from context menu\n"); + // Hide all windows to trigger app exit + native_window_list_t windows = native_window_manager_get_all(); + for (long i = 0; i < windows.count; i++) { + native_window_hide(windows.windows[i]); + } + native_window_list_free(&windows); +} + +void on_tools_submenu_event(const native_menu_event_t* event, void* user_data) { + (void)user_data; + if (event->type == NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_OPENED) { + printf("Tools submenu opened (ID: %u)\n", event->data.item_submenu_opened.item_id); + } else if (event->type == NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_CLOSED) { + printf("Tools submenu closed (ID: %u)\n", event->data.item_submenu_closed.item_id); + } +} + +native_menu_t create_context_menu(void) { + // Create context menu + native_menu_t context_menu = native_menu_create(); + + // Add Show Window item + native_menu_item_t show_window_item = + native_menu_item_create_with_label_and_type("Show Window", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(show_window_item, on_show_window_clicked, + NULL); + native_menu_add_item(context_menu, show_window_item); + + // Add Hide Window item + native_menu_item_t hide_window_item = + native_menu_item_create_with_label_and_type("Hide Window", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(hide_window_item, on_hide_window_clicked, + NULL); + native_menu_add_item(context_menu, hide_window_item); + + // Add Toggle Title Bar item + native_menu_item_t toggle_title_bar_item = + native_menu_item_create_with_label_and_type("Toggle Title Bar", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(toggle_title_bar_item, on_toggle_title_bar_clicked, + NULL); + native_menu_add_item(context_menu, toggle_title_bar_item); + + // Add separator + native_menu_add_separator(context_menu); + + // Add About item + native_menu_item_t about_item = native_menu_item_create_with_label_and_type("About", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(about_item, on_about_clicked, + NULL); + native_menu_add_item(context_menu, about_item); + + // Create Tools submenu + native_menu_t tools_submenu = native_menu_create(); + + // Add items to tools submenu + native_menu_item_t clear_cache_item = + native_menu_item_create_with_label_and_type("Clear Cache", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(clear_cache_item, on_clear_cache_clicked, + NULL); + native_menu_add_item(tools_submenu, clear_cache_item); + + native_menu_item_t reset_settings_item = + native_menu_item_create_with_label_and_type("Reset Settings", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(reset_settings_item, on_reset_settings_clicked, + NULL); + native_menu_add_item(tools_submenu, reset_settings_item); + + native_menu_add_separator(tools_submenu); + + native_menu_item_t debug_mode_item = + native_menu_item_create_with_label_and_type("Debug Mode", NATIVE_MENU_ITEM_TYPE_CHECKBOX); + native_menu_item_set_state(debug_mode_item, NATIVE_MENU_ITEM_STATE_UNCHECKED); + native_menu_item_add_listener(debug_mode_item, on_debug_mode_clicked, + (void*)(uintptr_t)debug_mode_item); + native_menu_add_item(tools_submenu, debug_mode_item); + + // Create the submenu parent item + native_menu_item_t tools_item = native_menu_item_create_with_label_and_type("Tools", NATIVE_MENU_ITEM_TYPE_SUBMENU); + native_menu_item_set_submenu(tools_item, tools_submenu); + + // Add submenu event listeners + native_menu_item_add_listener(tools_item, on_tools_submenu_event, NULL); + + native_menu_add_item(context_menu, tools_item); + + // Add separator before preferences + native_menu_add_separator(context_menu); + + // Add preferences item + native_menu_item_t preferences_item = + native_menu_item_create_with_label_and_type("Preferences", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_add_item(context_menu, preferences_item); + + // Add checkbox menu items + native_menu_item_t auto_start_item = + native_menu_item_create_with_label_and_type("Auto Start", NATIVE_MENU_ITEM_TYPE_CHECKBOX); + native_menu_item_set_state(auto_start_item, NATIVE_MENU_ITEM_STATE_CHECKED); // Initially checked + native_menu_item_add_listener(auto_start_item, on_auto_start_clicked, + (void*)(uintptr_t)auto_start_item); + native_menu_add_item(context_menu, auto_start_item); + + native_menu_item_t notifications_item = + native_menu_item_create_with_label_and_type("Show Notifications", NATIVE_MENU_ITEM_TYPE_CHECKBOX); + native_menu_item_set_state(notifications_item, + NATIVE_MENU_ITEM_STATE_UNCHECKED); // Initially unchecked + native_menu_item_add_listener(notifications_item, on_notifications_clicked, + (void*)(uintptr_t)notifications_item); + native_menu_add_item(context_menu, notifications_item); + + // Add three-state checkbox example + native_menu_item_t sync_item = + native_menu_item_create_with_label_and_type("Sync Status", NATIVE_MENU_ITEM_TYPE_CHECKBOX); + native_menu_item_set_state(sync_item, + NATIVE_MENU_ITEM_STATE_MIXED); // Initially mixed/indeterminate + native_menu_item_add_listener(sync_item, on_sync_item_clicked, (void*)(uintptr_t)sync_item); + native_menu_add_item(context_menu, sync_item); + + // Add separator before radio group + native_menu_add_separator(context_menu); + + // Add radio button group for theme selection + native_menu_item_t theme_label = native_menu_item_create_with_label_and_type("Theme:", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_add_item(context_menu, theme_label); + + native_menu_item_t light_theme_item = + native_menu_item_create_with_label_and_type("Light Theme", NATIVE_MENU_ITEM_TYPE_RADIO); + native_menu_item_set_radio_group(light_theme_item, 0); // Group 0 + native_menu_item_set_state(light_theme_item, + NATIVE_MENU_ITEM_STATE_CHECKED); // Default selection + native_menu_item_add_listener(light_theme_item, on_light_theme_clicked, + (void*)(uintptr_t)light_theme_item); + native_menu_add_item(context_menu, light_theme_item); + + native_menu_item_t dark_theme_item = + native_menu_item_create_with_label_and_type("Dark Theme", NATIVE_MENU_ITEM_TYPE_RADIO); + native_menu_item_set_radio_group(dark_theme_item, + 0); // Same group as light theme + native_menu_item_add_listener(dark_theme_item, on_dark_theme_clicked, + (void*)(uintptr_t)dark_theme_item); + native_menu_add_item(context_menu, dark_theme_item); + + native_menu_item_t auto_theme_item = + native_menu_item_create_with_label_and_type("Auto Theme", NATIVE_MENU_ITEM_TYPE_RADIO); + native_menu_item_set_radio_group(auto_theme_item, 0); // Same group + native_menu_item_add_listener(auto_theme_item, on_auto_theme_clicked, + (void*)(uintptr_t)auto_theme_item); + native_menu_add_item(context_menu, auto_theme_item); + + // Add another separator + native_menu_add_separator(context_menu); + + // Add exit item + native_menu_item_t exit_item = native_menu_item_create_with_label_and_type("Exit", NATIVE_MENU_ITEM_TYPE_NORMAL); + native_menu_item_add_listener(exit_item, on_exit_clicked, + NULL); + native_menu_add_item(context_menu, exit_item); + + return context_menu; +} + +int main() { + // Create a new window with default settings + g_window = native_window_create(); + + // Configure the window + native_window_set_title(g_window, "Window Example"); + native_size_t size = {800, 600}; + native_size_t minimum_size = {400, 300}; + native_size_t maximum_size = {1920, 1080}; + native_window_set_size(g_window, size, false); + native_window_set_minimum_size(g_window, minimum_size); + native_window_set_maximum_size(g_window, maximum_size); + native_window_center(g_window); + + // Create tray icon + g_tray_icon = native_tray_icon_create(); + if (g_tray_icon != NATIVE_INVALID_TRAY_ICON) { + // Create image from base64 data + native_image_t tray_image = native_image_from_base64( + + "data:image/" + "png;base64," + "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGPC/" + "xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVY" + "SWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAw" + "AAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEA" + "AKACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAABB553+" + "AAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPH" + "g6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg" + "Ni4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OT" + "kvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjph" + "Ym91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3" + "RpZmYvMS4wLyI+" + "CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+" + "CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+" + "CjwveDp4bXBtZXRhPgoZXuEHAAADOElEQVQ4EY1USUtbURQ+N+/" + "Fh1O04IxYHFAQNKiIIOJGV10oVbEb3fgHJLpKf0DjQuim3RS3roR25c7qzoWgkCAiiAM4I" + "W5MnPUlt+c75j6iCdILN+/cM3xnzFFEpPjqYDD4WWsd4htUSgXAS8v4k3VExroJ1o3y/" + "R6NRv+wlrKg2t7e/s2yrB+2bX8sKChwwHNdl/" + "Xg6+WwMTmOQ+Alk0mR+Xw+Bzb8+FJRUeFcXFz8VYiMBb/ZzE0kEnp/" + "f99mWnV2dtLz87MAAMzv99PR0RGVlJRQaWkpQCkvL49F2oV+KpWy+Y5YlZWVv+" + "Dl6uoq2dra6p+enlYtLS20vr4uhqwkYCcnJzQ0NCQ8dkqFhYW0tbWlzs/" + "PrcfHx2RZWZnFAdRQW1tbvLe3FzVJzc/Pw6OOxWJ4a/C5HLqvr0/" + "ex8fHenV1VWjIFxcX9cHBgR4YGEg1Nzfrjo6OuM35BwCCsPnKQTo4SJNrQysrKzQxMUG1t" + "bVSRxGm5aDZXBrLZMD3FgwK5uzs7AhYXV0dhUIhYZeXl9PS0pLQ4+" + "Pj1NDQQLu7u1RUVKS4kdrHEi8yA4RO4kxNTdHm5iZtb28TmvSSCNHY2BhdXl7S2toajY6O" + "UiAQINQarfcZYwOGr+" + "FVV1dTU1MTFRcXe2IDyk2gxsZG6TqPC3FjRQcRZh14w5mZmRGDcDhMp6en4gjOuLs0OTlJ" + "KMXy8jLV19cTd1psXmCFzP7p7++XVObm5kQYiUTo+" + "vqahoeHCWPE3ae7uztvXqGUM0IDDa83NzcEYIDy4NPe3p6ADQ4Oypsb4ZUIdrapiQHJ/" + "CI9GDw8PAh7YWGBzs7OhI7H45mqHo2UX82gJ0kTAEVa3d3dNDs7Kw3q6uoSJ6Z5GTYag22" + "GMmt8TPT8XxeAnp4e+Q+jFLnAGFghZaygV+uKN484hZEBBX1/f+/" + "xM6ICqVmOBZGwqqqqPrHRx/z8fPf29tbiJUH8f6XDw0PiVSZdfmOc6+lyFohi47/" + "WVy6ENA/1l/" + "XFg23zDhiRuqUXbBi1whJ9enqSQUWa7x3IcWHH0xDhLfUVYSpsWt6LMfZQwwX/" + "wLVwWPG97osM9Wf7Df6GGOwnsP4BQFiPuOZ8wJUAAAAASUVORK5CYII="); + + if (tray_image != NATIVE_INVALID_IMAGE) { + native_tray_icon_set_icon(g_tray_icon, tray_image); + native_image_free(tray_image); + } + + native_tray_icon_id_t tray_id = native_tray_icon_get_id(g_tray_icon); + printf("Tray ID: %u\n", tray_id); + + char* title = native_tray_icon_get_title(g_tray_icon); + if (title) { + printf("Tray Title: %s\n", title); + free_c_str(title); + } + + // Create context menu + g_context_menu = create_context_menu(); + + // Set the context menu to the tray icon + native_tray_icon_set_context_menu(g_tray_icon, g_context_menu); + + // Set context menu to trigger on left click + native_tray_icon_set_context_menu_trigger(g_tray_icon, NATIVE_CONTEXT_MENU_TRIGGER_CLICKED); + + // Set up event listeners + native_tray_icon_add_listener(g_tray_icon, on_tray_icon_event, NULL); + + native_tray_icon_set_visible(g_tray_icon, true); + } else { + fprintf(stderr, "Failed to create tray.\n"); + } + + // Run the application with the window + int result = native_application_run_with_window(g_window); + + // Cleanup: releasing a handle drops this caller's reference. + if (g_context_menu != NATIVE_INVALID_MENU) { + native_menu_free(g_context_menu); + } + if (g_tray_icon != NATIVE_INVALID_TRAY_ICON) { + native_tray_icon_free(g_tray_icon); + } + native_window_free(g_window); + + return result; +} diff --git a/packages/cnativeapi/cxx_impl/examples/window_example/CMakeLists.txt b/packages/cnativeapi/cxx_impl/examples/window_example/CMakeLists.txt new file mode 100644 index 0000000..b55e35e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/window_example/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.10) + +project(window_example VERSION 0.0.1 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add example program +add_executable(window_example + "main.cpp" +) + +# Link main library +target_link_libraries(window_example PRIVATE nativeapi) + +# Set example program properties +set_target_properties(window_example PROPERTIES + OUTPUT_NAME "window_example" +) + +# Set example program compile options (macOS only) +if(APPLE) + set_source_files_properties("main.cpp" + PROPERTIES + COMPILE_FLAGS "-x objective-c++" + ) +endif() diff --git a/packages/cnativeapi/cxx_impl/examples/window_example/main.cpp b/packages/cnativeapi/cxx_impl/examples/window_example/main.cpp new file mode 100644 index 0000000..ab30a56 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/examples/window_example/main.cpp @@ -0,0 +1,334 @@ +#include +#include "nativeapi.h" + +using nativeapi::Application; +using nativeapi::Display; +using nativeapi::DisplayAddedEvent; +using nativeapi::DisplayManager; +using nativeapi::DisplayRemovedEvent; +using nativeapi::Menu; +using nativeapi::MenuClosedEvent; +using nativeapi::MenuItem; +using nativeapi::MenuItemClickedEvent; +using nativeapi::MenuItemState; +using nativeapi::MenuItemSubmenuClosedEvent; +using nativeapi::MenuItemSubmenuOpenedEvent; +using nativeapi::MenuItemType; +using nativeapi::MenuOpenedEvent; +using nativeapi::TrayIcon; +using nativeapi::TrayIconClickedEvent; +using nativeapi::TrayIconDoubleClickedEvent; +using nativeapi::TrayIconRightClickedEvent; +using nativeapi::TrayManager; +using nativeapi::Window; +using nativeapi::WindowManager; + +int main() { + native_accessibility_manager_enable(); + bool is_enabled = native_accessibility_manager_is_enabled(); + std::cout << "is_enabled: " << is_enabled << std::endl; + + DisplayManager& display_manager = DisplayManager::GetInstance(); + TrayManager& tray_manager = TrayManager::GetInstance(); + WindowManager& window_manager = WindowManager::GetInstance(); + + // Create a new window (automatically registered) + std::shared_ptr window_ptr = std::make_shared(); + window_ptr->SetTitle("Window Example"); + window_ptr->SetSize({800, 600}, false); + window_ptr->SetMinimumSize({400, 300}); + window_ptr->SetMaximumSize({1920, 1080}); + window_ptr->Center(); + + std::shared_ptr tray_icon_ptr = std::make_shared(); + if (tray_icon_ptr != nullptr) { + TrayIcon& tray_icon = *tray_icon_ptr; + auto icon = nativeapi::Image::FromBase64( + "data:image/" + "png;base64," + "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGPC/" + "xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVY" + "SWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAw" + "AAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEA" + "AKACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAABB553+" + "AAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPH" + "g6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg" + "Ni4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OT" + "kvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjph" + "Ym91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3" + "RpZmYvMS4wLyI+" + "CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+" + "CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+" + "CjwveDp4bXBtZXRhPgoZXuEHAAADOElEQVQ4EY1USUtbURQ+N+/" + "Fh1O04IxYHFAQNKiIIOJGV10oVbEb3fgHJLpKf0DjQuim3RS3roR25c7qzoWgkCAiiAM4I" + "W5MnPUlt+c75j6iCdILN+/cM3xnzFFEpPjqYDD4WWsd4htUSgXAS8v4k3VExroJ1o3y/" + "R6NRv+wlrKg2t7e/s2yrB+2bX8sKChwwHNdl/" + "Xg6+WwMTmOQ+Alk0mR+Xw+Bzb8+FJRUeFcXFz8VYiMBb/ZzE0kEnp/" + "f99mWnV2dtLz87MAAMzv99PR0RGVlJRQaWkpQCkvL49F2oV+KpWy+Y5YlZWVv+" + "Dl6uoq2dra6p+enlYtLS20vr4uhqwkYCcnJzQ0NCQ8dkqFhYW0tbWlzs/" + "PrcfHx2RZWZnFAdRQW1tbvLe3FzVJzc/Pw6OOxWJ4a/C5HLqvr0/" + "ex8fHenV1VWjIFxcX9cHBgR4YGEg1Nzfrjo6OuM35BwCCsPnKQTo4SJNrQysrKzQxMUG1t" + "bVSRxGm5aDZXBrLZMD3FgwK5uzs7AhYXV0dhUIhYZeXl9PS0pLQ4+" + "Pj1NDQQLu7u1RUVKS4kdrHEi8yA4RO4kxNTdHm5iZtb28TmvSSCNHY2BhdXl7S2toajY6O" + "UiAQINQarfcZYwOGr+" + "FVV1dTU1MTFRcXe2IDyk2gxsZG6TqPC3FjRQcRZh14w5mZmRGDcDhMp6en4gjOuLs0OTlJ" + "KMXy8jLV19cTd1psXmCFzP7p7++XVObm5kQYiUTo+" + "vqahoeHCWPE3ae7uztvXqGUM0IDDa83NzcEYIDy4NPe3p6ADQ4Oypsb4ZUIdrapiQHJ/" + "CI9GDw8PAh7YWGBzs7OhI7H45mqHo2UX82gJ0kTAEVa3d3dNDs7Kw3q6uoSJ6Z5GTYag22" + "GMmt8TPT8XxeAnp4e+Q+jFLnAGFghZaygV+uKN484hZEBBX1/f+/" + "xM6ICqVmOBZGwqqqqPrHRx/z8fPf29tbiJUH8f6XDw0PiVSZdfmOc6+lyFohi47/" + "WVy6ENA/1l/" + "XFg23zDhiRuqUXbBi1whJ9enqSQUWa7x3IcWHH0xDhLfUVYSpsWt6LMfZQwwX/" + "wLVwWPG97osM9Wf7Df6GGOwnsP4BQFiPuOZ8wJUAAAAASUVORK5CYII="); + tray_icon.SetIcon(icon); + std::cout << "Tray ID: " << tray_icon.GetId() << std::endl; + auto title = tray_icon.GetTitle(); + std::cout << "Tray Title: " << (title.has_value() ? title.value() : "(no title)") << std::endl; + tray_icon.SetVisible(true); + + // Create context menu + auto context_menu = std::make_shared(); + + context_menu->AddListener( + [](const MenuOpenedEvent& event) { std::cout << "Menu opened" << std::endl; }); + + context_menu->AddListener( + [](const MenuClosedEvent& event) { std::cout << "Menu closed" << std::endl; }); + + // Add menu items + auto show_window_item = std::make_shared("Show Window", MenuItemType::Normal); + show_window_item->AddListener( + [window_ptr](const MenuItemClickedEvent& event) { + std::cout << "Show Window clicked from context menu" << std::endl; + if (window_ptr) { + window_ptr->Show(); + window_ptr->Focus(); + } + }); + context_menu->AddItem(show_window_item); + + auto hide_window_item = std::make_shared("Hide Window", MenuItemType::Normal); + hide_window_item->AddListener( + [window_ptr](const MenuItemClickedEvent& event) { + std::cout << "Hide Window clicked from context menu" << std::endl; + if (window_ptr) { + window_ptr->Hide(); + } + }); + context_menu->AddItem(hide_window_item); + + // Add separator + context_menu->AddSeparator(); + + // Add about item + auto about_item = std::make_shared("About", MenuItemType::Normal); + about_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "About clicked from context menu" << std::endl; + std::cout << "Window Example v1.0 - Native API Demo" << std::endl; + }); + context_menu->AddItem(about_item); + + // Create Tools submenu with submenu event handling + auto tools_submenu = std::make_shared(); + + // Add items to tools submenu + auto clear_cache_item = std::make_shared("Clear Cache", MenuItemType::Normal); + clear_cache_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "Clear Cache clicked from submenu" << std::endl; + }); + tools_submenu->AddItem(clear_cache_item); + + auto reset_settings_item = std::make_shared("Reset Settings", MenuItemType::Normal); + reset_settings_item->AddListener([](const MenuItemClickedEvent& event) { + std::cout << "Reset Settings clicked from submenu" << std::endl; + }); + tools_submenu->AddItem(reset_settings_item); + + tools_submenu->AddSeparator(); + + auto debug_mode_item = std::make_shared("Debug Mode", MenuItemType::Checkbox); + debug_mode_item->SetState(MenuItemState::Unchecked); + debug_mode_item->AddListener([debug_mode_item]( + const MenuItemClickedEvent& event) { + auto current_state = debug_mode_item->GetState(); + MenuItemState new_state = (current_state == MenuItemState::Checked) ? MenuItemState::Unchecked + : MenuItemState::Checked; + debug_mode_item->SetState(new_state); + std::cout << "Debug Mode " << (new_state == MenuItemState::Checked ? "enabled" : "disabled") + << std::endl; + }); + tools_submenu->AddItem(debug_mode_item); + + // Create the submenu parent item + auto tools_item = std::make_shared("Tools", MenuItemType::Submenu); + tools_item->SetSubmenu(tools_submenu); + + // Add submenu event listeners + tools_item->AddListener( + [](const MenuItemSubmenuOpenedEvent& event) { + std::cout << "Tools submenu opened (ID: " << event.GetItemId() << ")" << std::endl; + }); + + tools_item->AddListener( + [](const MenuItemSubmenuClosedEvent& event) { + std::cout << "Tools submenu closed (ID: " << event.GetItemId() << ")" << std::endl; + }); + + context_menu->AddItem(tools_item); + + // Add separator before preferences + context_menu->AddSeparator(); + + // Add preferences section (not a submenu, just a label) + auto preferences_item = std::make_shared("Preferences", MenuItemType::Normal); + context_menu->AddItem(preferences_item); + + // Add checkbox menu items + auto auto_start_item = std::make_shared("Auto Start", MenuItemType::Checkbox); + auto_start_item->SetState(MenuItemState::Checked); // Initially checked + auto_start_item->AddListener([auto_start_item]( + const MenuItemClickedEvent& event) { + auto current_state = auto_start_item->GetState(); + MenuItemState new_state = (current_state == MenuItemState::Checked) ? MenuItemState::Unchecked + : MenuItemState::Checked; + auto_start_item->SetState(new_state); + std::cout << "Auto Start " << (new_state == MenuItemState::Checked ? "enabled" : "disabled") + << std::endl; + }); + context_menu->AddItem(auto_start_item); + + auto notifications_item = + std::make_shared("Show Notifications", MenuItemType::Checkbox); + notifications_item->SetState(MenuItemState::Unchecked); // Initially unchecked + notifications_item->AddListener([notifications_item]( + const MenuItemClickedEvent& event) { + auto current_state = notifications_item->GetState(); + MenuItemState new_state = (current_state == MenuItemState::Checked) ? MenuItemState::Unchecked + : MenuItemState::Checked; + notifications_item->SetState(new_state); + std::cout << "Notifications " + << (new_state == MenuItemState::Checked ? "enabled" : "disabled") << std::endl; + }); + context_menu->AddItem(notifications_item); + + // Add three-state checkbox example + auto sync_item = std::make_shared("Sync Status", MenuItemType::Checkbox); + sync_item->SetState(MenuItemState::Mixed); // Initially mixed/indeterminate + sync_item->AddListener([sync_item](const MenuItemClickedEvent& event) { + auto current_state = sync_item->GetState(); + MenuItemState next_state; + std::string state_name; + + // Cycle through states: Mixed -> Checked -> Unchecked -> Mixed + switch (current_state) { + case MenuItemState::Mixed: + next_state = MenuItemState::Checked; + state_name = "enabled"; + break; + case MenuItemState::Checked: + next_state = MenuItemState::Unchecked; + state_name = "disabled"; + break; + case MenuItemState::Unchecked: + default: + next_state = MenuItemState::Mixed; + state_name = "partial"; + break; + } + + sync_item->SetState(next_state); + std::cout << "Sync Status: " << state_name << std::endl; + }); + context_menu->AddItem(sync_item); + + // Add separator before radio group + context_menu->AddSeparator(); + + // Add radio button group for theme selection + auto theme_label = std::make_shared("Theme:", MenuItemType::Normal); + context_menu->AddItem(theme_label); + + auto light_theme_item = std::make_shared("Light Theme", MenuItemType::Radio); + light_theme_item->SetRadioGroup(0); // Group 0 + light_theme_item->SetState(MenuItemState::Checked); // Default selection + light_theme_item->AddListener( + [light_theme_item](const MenuItemClickedEvent& event) { + light_theme_item->SetState(MenuItemState::Checked); + std::cout << "Light theme selected" << std::endl; + }); + context_menu->AddItem(light_theme_item); + + auto dark_theme_item = std::make_shared("Dark Theme", MenuItemType::Radio); + dark_theme_item->SetRadioGroup(0); // Same group as light theme + dark_theme_item->AddListener( + [dark_theme_item](const MenuItemClickedEvent& event) { + dark_theme_item->SetState(MenuItemState::Checked); + std::cout << "Dark theme selected" << std::endl; + }); + context_menu->AddItem(dark_theme_item); + + auto auto_theme_item = std::make_shared("Auto Theme", MenuItemType::Radio); + auto_theme_item->SetRadioGroup(0); // Same group + auto_theme_item->AddListener( + [auto_theme_item](const MenuItemClickedEvent& event) { + auto_theme_item->SetState(MenuItemState::Checked); + std::cout << "Auto theme selected" << std::endl; + }); + context_menu->AddItem(auto_theme_item); + + // Add another separator + context_menu->AddSeparator(); + + // Add exit item + auto exit_item = std::make_shared("Exit", MenuItemType::Normal); + exit_item->AddListener([window_ptr](const MenuItemClickedEvent& event) { + std::cout << "Exit clicked from context menu" << std::endl; + // Close the window to trigger app exit + if (window_ptr) { + window_ptr->Hide(); + } + }); + context_menu->AddItem(exit_item); + + // Set the context menu to the tray icon + tray_icon.SetContextMenu(context_menu); + + // Set up event listeners + tray_icon.AddListener([&tray_icon](const TrayIconClickedEvent& event) { + std::cout << "*** TRAY ICON LEFT CLICKED! ***" << std::endl; + std::cout << "This is the left click handler working!" << std::endl; + std::cout << "Tray icon ID: " << event.GetTrayIconId() << std::endl; + + // Open context menu on left click + tray_icon.OpenContextMenu(); + }); + + tray_icon.AddListener([](const TrayIconRightClickedEvent& event) { + std::cout << "*** TRAY ICON RIGHT CLICKED! ***" << std::endl; + std::cout << "This is the right click handler working!" << std::endl; + std::cout << "Tray icon ID: " << event.GetTrayIconId() << std::endl; + }); + + tray_icon.AddListener([](const TrayIconDoubleClickedEvent& event) { + std::cout << "*** TRAY ICON DOUBLE CLICKED! ***" << std::endl; + std::cout << "This is the double click handler working!" << std::endl; + std::cout << "Tray icon ID: " << event.GetTrayIconId() << std::endl; + }); + } else { + std::cerr << "Failed to create tray." << std::endl; + } + + display_manager.AddListener( + [](const nativeapi::DisplayAddedEvent& event) { + std::cout << "Display added: " << event.GetDisplay()->GetId() << std::endl; + }); + display_manager.AddListener( + [](const nativeapi::DisplayRemovedEvent& event) { + std::cout << "Display removed: " << event.GetDisplay()->GetId() << std::endl; + }); + + auto& app = Application::GetInstance(); + app.Run(window_ptr); + + return 0; +} diff --git a/packages/cnativeapi/cxx_impl/include/nativeapi.h b/packages/cnativeapi/cxx_impl/include/nativeapi.h new file mode 100644 index 0000000..cf3657b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/include/nativeapi.h @@ -0,0 +1,60 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#ifdef __cplusplus +// C++ API +#include "../src/accessibility_manager.h" +#include "../src/application.h" +#include "../src/dialog.h" +#include "../src/display.h" +#include "../src/display_manager.h" +#include "../src/foundation/color.h" +#include "../src/foundation/geometry.h" +#include "../src/foundation/keyboard.h" +#include "../src/image.h" +#include "../src/keyboard_monitor.h" +#include "../src/launch_at_login.h" +#include "../src/menu.h" +#include "../src/message_dialog.h" +#include "../src/placement.h" +#include "../src/positioning_strategy.h" +#include "../src/preferences.h" +#include "../src/secure_storage.h" +#include "../src/shortcut.h" +#include "../src/shortcut_manager.h" +#include "../src/tray_icon.h" +#include "../src/tray_manager.h" +#include "../src/url_opener.h" +#include "../src/window.h" +#include "../src/window_manager.h" +#endif + +// C API (usable from both C and C++) +#include "../src/capi/accessibility_manager_c.h" +#include "../src/capi/application_c.h" +#include "../src/capi/color_c.h" +#include "../src/capi/common_c.h" +#include "../src/capi/dialog_c.h" +#include "../src/capi/display_c.h" +#include "../src/capi/display_manager_c.h" +#include "../src/capi/geometry_c.h" +#include "../src/capi/image_c.h" +#include "../src/capi/keyboard_c.h" +#include "../src/capi/keyboard_monitor_c.h" +#include "../src/capi/launch_at_login_c.h" +#include "../src/capi/menu_c.h" +#include "../src/capi/message_dialog_c.h" +#include "../src/capi/placement_c.h" +#include "../src/capi/positioning_strategy_c.h" +#include "../src/capi/preferences_c.h" +#include "../src/capi/secure_storage_c.h" +#include "../src/capi/shortcut_c.h" +#include "../src/capi/shortcut_manager_c.h" +#include "../src/capi/string_utils_c.h" +#include "../src/capi/tray_icon_c.h" +#include "../src/capi/tray_manager_c.h" +#include "../src/capi/url_opener_c.h" +#include "../src/capi/window_c.h" +#include "../src/capi/window_manager_c.h" diff --git a/packages/cnativeapi/cxx_impl/src/CMakeLists.txt b/packages/cnativeapi/cxx_impl/src/CMakeLists.txt new file mode 100644 index 0000000..2e811fb --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/CMakeLists.txt @@ -0,0 +1,94 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +project(nativeapi VERSION 0.0.1 LANGUAGES CXX) + +# Enable Objective-C++ +if(APPLE) + enable_language(OBJCXX) +endif() + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Common source files +file(GLOB COMMON_SOURCES "*.cpp" "foundation/*.cpp") +list(FILTER COMMON_SOURCES EXCLUDE REGEX "platform/*") + +# C API source files +file(GLOB CAPI_SOURCES "capi/*.cpp" "capi/*.c") + +# Platform-specific source files +if(ANDROID) + file(GLOB PLATFORM_SOURCES "platform/android/*.cpp") +elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") + file(GLOB PLATFORM_SOURCES "platform/ios/*.mm") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + file(GLOB PLATFORM_SOURCES "platform/linux/*.cpp") + # Find packages for Linux + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11) + pkg_check_modules(XI REQUIRED IMPORTED_TARGET xi) +elseif(APPLE) + file(GLOB PLATFORM_SOURCES "platform/macos/*.mm") +elseif(CMAKE_SYSTEM_NAME STREQUAL "OHOS") + file(GLOB PLATFORM_SOURCES "platform/ohos/*.cpp") +elseif(WIN32) + file(GLOB PLATFORM_SOURCES "platform/windows/*.cpp") +else() + set(PLATFORM_SOURCES "") +endif() + +# Add library target +add_library(nativeapi STATIC + ${COMMON_SOURCES} + ${PLATFORM_SOURCES} + ${CAPI_SOURCES} +) + +# Set library properties +# Propagate the language requirement to anything that links nativeapi. +# +# The CMAKE_CXX_STANDARD variables above are directory-scoped: they govern this +# subdirectory's own targets and nothing else. Consumers (tests, examples, and +# any downstream project doing add_subdirectory or find_package) were therefore +# compiled at the toolchain default, which is how a header using std::optional +# could fail to compile in a target that links this library. +target_compile_features(nativeapi PUBLIC cxx_std_17) + +set_target_properties(nativeapi PROPERTIES + PUBLIC_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/../include/**/*.h" +) + +# Set library include directories +target_include_directories(nativeapi PUBLIC + $ + $ +) + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_include_directories(nativeapi PUBLIC ${GTK_INCLUDE_DIRS}) +endif () + +# Link required frameworks and libraries +if(ANDROID) + target_link_libraries(nativeapi PUBLIC log android) +elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") + target_link_libraries(nativeapi PUBLIC "-framework UIKit" "-framework Foundation" "-framework CoreGraphics") + target_compile_options(nativeapi PRIVATE "-x" "objective-c++") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_libraries(nativeapi PUBLIC PkgConfig::GTK PkgConfig::X11 PkgConfig::XI pthread) +elseif(APPLE) + target_link_libraries(nativeapi PUBLIC "-framework Cocoa") + target_link_libraries(nativeapi PUBLIC "-framework Carbon") + target_link_libraries(nativeapi PUBLIC "-framework ServiceManagement") + target_compile_options(nativeapi PRIVATE "-x" "objective-c++") +elseif(CMAKE_SYSTEM_NAME STREQUAL "OHOS") + target_link_libraries(nativeapi PUBLIC hilog_ndk.z) +elseif(WIN32) + target_link_libraries(nativeapi PUBLIC user32 shell32 dwmapi gdiplus crypt32 advapi32) +endif () diff --git a/packages/cnativeapi/cxx_impl/src/accessibility_manager.cpp b/packages/cnativeapi/cxx_impl/src/accessibility_manager.cpp new file mode 100644 index 0000000..28a457d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/accessibility_manager.cpp @@ -0,0 +1,14 @@ +#include "accessibility_manager.h" + +namespace nativeapi { + +AccessibilityManager& AccessibilityManager::GetInstance() { + static AccessibilityManager instance; + return instance; +} + +AccessibilityManager::AccessibilityManager() : enabled_(false) {} + +AccessibilityManager::~AccessibilityManager() {} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/accessibility_manager.h b/packages/cnativeapi/cxx_impl/src/accessibility_manager.h new file mode 100644 index 0000000..ddae524 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/accessibility_manager.h @@ -0,0 +1,91 @@ +#pragma once + +namespace nativeapi { + +/** + * @class AccessibilityManager + * @brief A singleton class that manages system accessibility features + * + * The AccessibilityManager provides a centralized interface for managing + * accessibility functionality across the system. It follows the singleton + * pattern to ensure only one instance exists throughout the application + * lifecycle, providing consistent state management for accessibility features. + * + * Key responsibilities: + * - Enable/disable system accessibility features + * - Query accessibility state + * - Provide thread-safe access to accessibility functionality + * + * Usage example: + * @code + * AccessibilityManager& manager = AccessibilityManager::GetInstance(); + * manager.Enable(); + * bool enabled = manager.IsEnabled(); + * @endcode + */ +class AccessibilityManager { + public: + /** + * @brief Gets the singleton instance of AccessibilityManager + * @return Reference to the singleton instance + * + * This method provides thread-safe access to the singleton instance. + * The instance is created on first access and remains alive for the + * duration of the application. + */ + static AccessibilityManager& GetInstance(); + + /** + * @brief Virtual destructor + * + * Ensures proper cleanup of resources when the manager is destroyed. + * Note: In singleton pattern, this is typically called only at application + * shutdown. + */ + virtual ~AccessibilityManager(); + + /** + * @brief Enables system accessibility features + * + * Activates accessibility functionality across the system. This method + * should be called to make accessibility features available to users. + * The operation is idempotent - calling it multiple times has the same + * effect as calling it once. + * + * @note This operation may require system permissions depending on the + * platform implementation. + */ + void Enable(); + + /** + * @brief Checks if accessibility features are currently enabled + * @return true if accessibility is enabled, false otherwise + * + * This method provides a quick way to query the current state of + * accessibility features without modifying the system state. + */ + bool IsEnabled(); + + // Delete copy constructor and assignment operator to prevent copies + AccessibilityManager(const AccessibilityManager&) = delete; + AccessibilityManager& operator=(const AccessibilityManager&) = delete; + + private: + /** + * @brief Private constructor for singleton pattern + * + * Initializes the AccessibilityManager instance. This constructor is + * private to prevent direct instantiation - use GetInstance() instead. + */ + AccessibilityManager(); + + /** + * @brief Internal flag tracking accessibility state + * + * Maintains the current enabled/disabled state of accessibility features. + * This member is used internally by Enable() and IsEnabled() methods. + */ + bool enabled_; +}; + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/application.cpp b/packages/cnativeapi/cxx_impl/src/application.cpp new file mode 100644 index 0000000..0b2670f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/application.cpp @@ -0,0 +1,14 @@ +#include "application.h" + +namespace nativeapi { + +Application& Application::GetInstance() { + static Application instance; + return instance; +} + +int RunApp(std::shared_ptr window) { + return Application::GetInstance().Run(window); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/application.h b/packages/cnativeapi/cxx_impl/src/application.h new file mode 100644 index 0000000..4ae85c0 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/application.h @@ -0,0 +1,309 @@ +#pragma once + +#include +#include +#include +#include + +#include "foundation/event.h" +#include "foundation/event_emitter.h" +#include "foundation/geometry.h" +#include "menu.h" +#include "window.h" + +namespace nativeapi { + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * @brief Application lifecycle events + */ +class ApplicationEvent : public Event { + public: + ApplicationEvent() = default; + virtual ~ApplicationEvent() = default; +}; + +/** + * @brief Event emitted when the application starts + */ +class ApplicationStartedEvent : public ApplicationEvent { + public: + ApplicationStartedEvent() = default; + std::string GetTypeName() const override { return "ApplicationStartedEvent"; } +}; + +/** + * @brief Event emitted when the application is about to exit + */ +class ApplicationExitingEvent : public ApplicationEvent { + public: + ApplicationExitingEvent(int exit_code) : exit_code_(exit_code) {} + + int GetExitCode() const { return exit_code_; } + std::string GetTypeName() const override { return "ApplicationExitingEvent"; } + + private: + int exit_code_; +}; + +/** + * @brief Event emitted when the application is activated (brought to foreground) + */ +class ApplicationActivatedEvent : public ApplicationEvent { + public: + ApplicationActivatedEvent() = default; + std::string GetTypeName() const override { return "ApplicationActivatedEvent"; } +}; + +/** + * @brief Event emitted when the application is deactivated (sent to background) + */ +class ApplicationDeactivatedEvent : public ApplicationEvent { + public: + ApplicationDeactivatedEvent() = default; + std::string GetTypeName() const override { return "ApplicationDeactivatedEvent"; } +}; + +/** + * @brief Event emitted when the application receives a quit request + */ +class ApplicationQuitRequestedEvent : public ApplicationEvent { + public: + ApplicationQuitRequestedEvent() = default; + std::string GetTypeName() const override { return "ApplicationQuitRequestedEvent"; } +}; + +/** + * @brief Application is a singleton class that manages the application lifecycle + * + * The Application class provides centralized management of application-wide state, + * lifecycle events, and coordination between different managers. It follows the + * singleton pattern to ensure there's only one application instance throughout + * the application lifetime. + * + * Key features: + * - Singleton pattern ensures single application instance + * - Event-driven architecture for application lifecycle notifications + * - Cross-platform application management + * - Integration with existing managers (WindowManager, DisplayManager, etc.) + * - Thread-safe access to the singleton instance + * - Automatic cleanup of resources on destruction + * + * @note This class is thread-safe for singleton access, but individual operations + * may require additional synchronization depending on the platform implementation. + */ +class Application : public EventEmitter { + public: + /** + * @brief Get the singleton instance of Application + * + * This method provides access to the unique instance of Application using + * the Meyer's singleton pattern. The instance is created on first call and + * remains alive for the duration of the application. This method is thread-safe + * and guarantees that only one instance will be created even in multi-threaded + * environments. + * + * @return Reference to the singleton Application instance + * @thread_safety This method is thread-safe + * + * @code + * // Usage example: + * auto& app = Application::GetInstance(); + * int exit_code = app.Run(); + * @endcode + */ + static Application& GetInstance(); + + /** + * @brief Destructor + * + * Cleans up all resources, stops event monitoring, and performs final cleanup. + * This is automatically called when the application terminates. + */ + virtual ~Application(); + + /** + * @brief Run the application main event loop + * + * Starts the main event loop and blocks until the application exits. + * This method handles platform-specific event processing and coordination + * between different managers. + * + * @return Exit code of the application (0 for success) + * + * @code + * auto& app = Application::GetInstance(); + * int exit_code = app.Run(); + * @endcode + */ + int Run(); + + /** + * @brief Run the application with the specified window + * + * Starts the main event loop with the given window and blocks until the + * application exits. This method sets the window as the primary window + * and starts the event loop. + * + * @param window The window to run the application with + * @return Exit code of the application (0 for success) + * + * @code + * auto& app = Application::GetInstance(); + * auto window = WindowManager::GetInstance().Create(options); + * int exit_code = app.Run(window); + * @endcode + */ + int Run(std::shared_ptr window); + + /** + * @brief Request the application to quit + * + * Initiates the application shutdown process. This method emits an + * ApplicationQuitRequestedEvent and begins the cleanup process. + * + * @param exit_code The exit code to use when quitting (default: 0) + * + * @code + * auto& app = Application::GetInstance(); + * app.Quit(0); // Quit with success code + * @endcode + */ + void Quit(int exit_code = 0); + + /** + * @brief Check if the application is currently running + * + * @return true if the application is running, false otherwise + */ + bool IsRunning() const; + + /** + * @brief Check if this is a single instance application + * + * @return true if only one instance is allowed, false otherwise + */ + bool IsSingleInstance() const; + + /** + * @brief Set the application icon + * + * Sets the application icon that appears in the dock (macOS), taskbar (Windows), + * or application list (Linux). + * + * @param icon_path Path to the icon file + * @return true if the icon was set successfully, false otherwise + */ + bool SetIcon(const std::string& icon_path); + + /** + * @brief Show or hide the dock icon (macOS only) + * + * Controls whether the application appears in the macOS dock. + * This method has no effect on other platforms. + * + * @param visible true to show the dock icon, false to hide it + * @return true if the operation succeeded, false otherwise + */ + bool SetDockIconVisible(bool visible); + + /** + * @brief Set the application menu bar + * + * Sets the application-wide menu bar that appears at the top of the screen. + * This is primarily used on macOS, but may have effects on other platforms. + * + * @param menu Shared pointer to the menu to set as the application menu + * @return true if the menu was set successfully, false otherwise + */ + bool SetMenuBar(std::shared_ptr menu); + + /** + * @brief Get the primary window of the application + * + * Returns the main window of the application, if one exists. + * + * @return Shared pointer to the primary window, or nullptr if none exists + */ + std::shared_ptr GetPrimaryWindow() const; + + /** + * @brief Set the primary window of the application + * + * Sets the main window of the application. This window will be used for + * application-level operations and may receive special treatment from + * the platform. + * + * @param window Shared pointer to the window to set as primary + */ + void SetPrimaryWindow(std::shared_ptr window); + + /** + * @brief Get all application windows + * + * Returns a vector containing all windows belonging to this application. + * + * @return Vector of shared pointers to all application windows + */ + std::vector> GetAllWindows() const; + + private: + /** + * @brief Private constructor to enforce singleton pattern + * + * Automatically initializes the Application instance and sets up platform-specific + * event monitoring. This constructor is private to prevent direct instantiation. + */ + Application(); + + // Prevent copy construction and assignment to maintain singleton property + Application(const Application&) = delete; + Application& operator=(const Application&) = delete; + Application(Application&&) = delete; + Application& operator=(Application&&) = delete; + + /** + * @brief Platform-specific implementation details + * + * Uses the PIMPL (Pointer to Implementation) idiom to hide platform-specific + * details and reduce compilation dependencies. + */ + class Impl; + std::unique_ptr pimpl_; + + /** + * @brief Application state + */ + bool initialized_; + bool running_; + int exit_code_; + + /** + * @brief Primary application window + */ + std::shared_ptr primary_window_; + + private: +}; + +/** + * @brief Convenience function to run the application with the specified window + * + * This is equivalent to calling Application::GetInstance().Run(window). + * This function provides a simple way to run an application without + * explicitly accessing the singleton. + * + * @param window The window to run the application with + * @return Exit code of the application (0 for success) + * + * @code + * auto window = WindowManager::GetInstance().Create(options); + * int exit_code = RunApp(window); + * @endcode + */ +int RunApp(std::shared_ptr window); + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.cpp new file mode 100644 index 0000000..ee1465a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.cpp @@ -0,0 +1,36 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "accessibility_manager_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../accessibility_manager.h" + +void native_accessibility_manager_enable(void) { + try { + nativeapi::AccessibilityManager::GetInstance().Enable(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_accessibility_manager_enable"); + return; + } +} + +bool native_accessibility_manager_is_enabled(void) { + try { + return nativeapi::AccessibilityManager::GetInstance().IsEnabled(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_accessibility_manager_is_enabled"); + return false; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.h b/packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.h new file mode 100644 index 0000000..aa9660a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/accessibility_manager_c.h @@ -0,0 +1,29 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +FFI_PLUGIN_EXPORT +void native_accessibility_manager_enable(void); + +FFI_PLUGIN_EXPORT +bool native_accessibility_manager_is_enabled(void); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/application_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/application_c.cpp new file mode 100644 index 0000000..6f0f29b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/application_c.cpp @@ -0,0 +1,205 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "application_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../window.h" +#include "window_c.h" +#include "../menu.h" +#include "menu_c.h" +#include "../application.h" + +int native_application_run(void) { + try { + return nativeapi::Application::GetInstance().Run(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_run"); + return 0; + } +} + +int native_application_run_with_window(native_window_t window) { + try { + auto window_cpp = nativeapi::HandleTable::GetInstance().Resolve(window); + return nativeapi::Application::GetInstance().Run(window_cpp); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_run_with_window"); + return 0; + } +} + +void native_application_quit(int exit_code) { + try { + nativeapi::Application::GetInstance().Quit(exit_code); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_quit"); + return; + } +} + +bool native_application_is_running(void) { + try { + return nativeapi::Application::GetInstance().IsRunning(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_is_running"); + return false; + } +} + +bool native_application_is_single_instance(void) { + try { + return nativeapi::Application::GetInstance().IsSingleInstance(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_is_single_instance"); + return false; + } +} + +bool native_application_set_icon(const char* icon_path) { + try { + return nativeapi::Application::GetInstance().SetIcon(std::string(icon_path ? icon_path : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_set_icon"); + return false; + } +} + +bool native_application_set_dock_icon_visible(bool visible) { + try { + return nativeapi::Application::GetInstance().SetDockIconVisible(visible); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_set_dock_icon_visible"); + return false; + } +} + +bool native_application_set_menu_bar(native_menu_t menu) { + try { + auto menu_cpp = nativeapi::HandleTable::GetInstance().Resolve(menu); + return nativeapi::Application::GetInstance().SetMenuBar(menu_cpp); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_set_menu_bar"); + return false; + } +} + +native_window_t native_application_get_primary_window(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::Application::GetInstance().GetPrimaryWindow()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_get_primary_window"); + return 0; + } +} + +void native_application_set_primary_window(native_window_t window) { + try { + auto window_cpp = nativeapi::HandleTable::GetInstance().Resolve(window); + nativeapi::Application::GetInstance().SetPrimaryWindow(window_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_set_primary_window"); + return; + } +} + +native_window_list_t native_application_get_all_windows(void) { + try { + const auto items = nativeapi::Application::GetInstance().GetAllWindows(); + native_window_list_t list = {}; + if (items.empty()) { + return list; + } + list.windows = new (std::nothrow) native_window_t[items.size()]; + if (!list.windows) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.windows[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_application_get_all_windows"); + native_window_list_t empty = {}; + return empty; + } +} + +native_listener_id_t native_application_add_listener(native_application_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + try { + return static_cast(nativeapi::Application::GetInstance().AddListener( + [callback, user_data](const nativeapi::ApplicationEvent& event) { + native_application_event_t c_event = {}; + if (!to_c_application_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_application_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_application_remove_listener(native_listener_id_t listener_id) { + try { + return nativeapi::Application::GetInstance().RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + +bool to_c_application_event(const nativeapi::ApplicationEvent& event, native_application_event_t* out) { + if (!out) { + return false; + } + *out = native_application_event_t{}; + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_APPLICATION_EVENT_TYPE_STARTED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_APPLICATION_EVENT_TYPE_EXITING; + out->data.exiting.exit_code = typed->GetExitCode(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_APPLICATION_EVENT_TYPE_ACTIVATED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_APPLICATION_EVENT_TYPE_DEACTIVATED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_APPLICATION_EVENT_TYPE_QUIT_REQUESTED; + (void)typed; + return true; + } + return false; +} + +void free_c_application_event(native_application_event_t* value) { + if (!value) { + return; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/application_c.h b/packages/cnativeapi/cxx_impl/src/capi/application_c.h new file mode 100644 index 0000000..d6116d8 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/application_c.h @@ -0,0 +1,105 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "menu_c.h" +#include "window_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Which concrete ApplicationEvent arrived. +typedef enum { + NATIVE_APPLICATION_EVENT_TYPE_STARTED = 0, + NATIVE_APPLICATION_EVENT_TYPE_EXITING = 1, + NATIVE_APPLICATION_EVENT_TYPE_ACTIVATED = 2, + NATIVE_APPLICATION_EVENT_TYPE_DEACTIVATED = 3, + NATIVE_APPLICATION_EVENT_TYPE_QUIT_REQUESTED = 4, +} native_application_event_type_t; + +/// One ApplicationEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_application_event_type_t type; + union { + struct { + int exit_code; + } exiting; + } data; +} native_application_event_t; + +typedef void (*native_application_event_callback_t)(const native_application_event_t* event, void* user_data); + +FFI_PLUGIN_EXPORT +int native_application_run(void); + +FFI_PLUGIN_EXPORT +int native_application_run_with_window(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_application_quit(int exit_code); + +FFI_PLUGIN_EXPORT +bool native_application_is_running(void); + +FFI_PLUGIN_EXPORT +bool native_application_is_single_instance(void); + +FFI_PLUGIN_EXPORT +bool native_application_set_icon(const char* icon_path); + +FFI_PLUGIN_EXPORT +bool native_application_set_dock_icon_visible(bool visible); + +FFI_PLUGIN_EXPORT +bool native_application_set_menu_bar(native_menu_t menu); + +/// Caller owns the returned handle; release it with native_window_free(). +FFI_PLUGIN_EXPORT +native_window_t native_application_get_primary_window(void); + +FFI_PLUGIN_EXPORT +void native_application_set_primary_window(native_window_t window); + +FFI_PLUGIN_EXPORT +native_window_list_t native_application_get_all_windows(void); + +/// Registers @p callback for every ApplicationEvent this Application emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_application_add_listener(native_application_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_application_remove_listener(native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class ApplicationEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_application_event(const nativeapi::ApplicationEvent& event, native_application_event_t* out); +/// Releases everything to_c_application_event() allocated. +void free_c_application_event(native_application_event_t* value); + +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/color_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/color_c.cpp new file mode 100644 index 0000000..9c72762 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/color_c.cpp @@ -0,0 +1,77 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "color_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/color.h" + +const native_color_t NATIVE_COLOR_TRANSPARENT = to_c_color(nativeapi::Color::Transparent); + +const native_color_t NATIVE_COLOR_BLACK = to_c_color(nativeapi::Color::Black); + +const native_color_t NATIVE_COLOR_WHITE = to_c_color(nativeapi::Color::White); + +const native_color_t NATIVE_COLOR_RED = to_c_color(nativeapi::Color::Red); + +const native_color_t NATIVE_COLOR_GREEN = to_c_color(nativeapi::Color::Green); + +const native_color_t NATIVE_COLOR_BLUE = to_c_color(nativeapi::Color::Blue); + +const native_color_t NATIVE_COLOR_YELLOW = to_c_color(nativeapi::Color::Yellow); + +const native_color_t NATIVE_COLOR_CYAN = to_c_color(nativeapi::Color::Cyan); + +const native_color_t NATIVE_COLOR_MAGENTA = to_c_color(nativeapi::Color::Magenta); + +native_color_t native_color_from_rgba(unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha) { + try { + const auto cpp_result = nativeapi::Color::FromRGBA(red, green, blue, alpha); + return to_c_color(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_color_from_rgba"); + native_color_t result = {}; + return result; + } +} + +native_color_t native_color_from_hex(const char* hex) { + try { + const auto cpp_result = nativeapi::Color::FromHex(hex); + return to_c_color(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_color_from_hex"); + native_color_t result = {}; + return result; + } +} + +unsigned int native_color_to_rgba(native_color_t color) { + try { + const auto self = to_cpp_color(color); + return self.ToRGBA(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_color_to_rgba"); + return 0; + } +} + +unsigned int native_color_to_argb(native_color_t color) { + try { + const auto self = to_cpp_color(color); + return self.ToARGB(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_color_to_argb"); + return 0; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/color_c.h b/packages/cnativeapi/cxx_impl/src/capi/color_c.h new file mode 100644 index 0000000..15fbed0 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/color_c.h @@ -0,0 +1,98 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; +} native_color_t; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_TRANSPARENT; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_BLACK; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_WHITE; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_RED; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_GREEN; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_BLUE; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_YELLOW; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_CYAN; + +FFI_PLUGIN_EXPORT +extern const native_color_t NATIVE_COLOR_MAGENTA; + +FFI_PLUGIN_EXPORT +native_color_t native_color_from_rgba(unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha); + +FFI_PLUGIN_EXPORT +native_color_t native_color_from_hex(const char* hex); + +FFI_PLUGIN_EXPORT +unsigned int native_color_to_rgba(native_color_t color); + +FFI_PLUGIN_EXPORT +unsigned int native_color_to_argb(native_color_t color); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../foundation/color.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_color_t to_c_color(const nativeapi::Color& value); +inline nativeapi::Color to_cpp_color(const native_color_t& value); + +inline native_color_t to_c_color(const nativeapi::Color& value) { + native_color_t result = {}; + result.r = value.r; + result.g = value.g; + result.b = value.b; + result.a = value.a; + return result; +} + +inline nativeapi::Color to_cpp_color(const native_color_t& value) { + nativeapi::Color result = {}; + result.r = value.r; + result.g = value.g; + result.b = value.b; + result.a = value.a; + return result; +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/common_c.h b/packages/cnativeapi/cxx_impl/src/capi/common_c.h new file mode 100644 index 0000000..d4e5b36 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/common_c.h @@ -0,0 +1,27 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Identifies one registered event listener. +typedef uint64_t native_listener_id_t; + +/// Returned by add_listener when registration failed. +#define NATIVE_INVALID_LISTENER_ID ((native_listener_id_t)0) + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/dialog_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/dialog_c.cpp new file mode 100644 index 0000000..fb0fb0b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/dialog_c.cpp @@ -0,0 +1,17 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "dialog_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../dialog.h" + diff --git a/packages/cnativeapi/cxx_impl/src/capi/dialog_c.h b/packages/cnativeapi/cxx_impl/src/capi/dialog_c.h new file mode 100644 index 0000000..f2681c1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/dialog_c.h @@ -0,0 +1,66 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + NATIVE_DIALOG_MODALITY_NONE = 0, + NATIVE_DIALOG_MODALITY_APPLICATION = 1, + NATIVE_DIALOG_MODALITY_WINDOW = 2, +} native_dialog_modality_t; + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../dialog.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_dialog_modality_t to_c_dialog_modality(nativeapi::DialogModality value); +inline nativeapi::DialogModality to_cpp_dialog_modality(native_dialog_modality_t value); + +inline native_dialog_modality_t to_c_dialog_modality(nativeapi::DialogModality value) { + switch (value) { + case nativeapi::DialogModality::None: + return NATIVE_DIALOG_MODALITY_NONE; + case nativeapi::DialogModality::Application: + return NATIVE_DIALOG_MODALITY_APPLICATION; + case nativeapi::DialogModality::Window: + return NATIVE_DIALOG_MODALITY_WINDOW; + default: + return NATIVE_DIALOG_MODALITY_NONE; + } +} + +inline nativeapi::DialogModality to_cpp_dialog_modality(native_dialog_modality_t value) { + switch (value) { + case NATIVE_DIALOG_MODALITY_NONE: + return nativeapi::DialogModality::None; + case NATIVE_DIALOG_MODALITY_APPLICATION: + return nativeapi::DialogModality::Application; + case NATIVE_DIALOG_MODALITY_WINDOW: + return nativeapi::DialogModality::Window; + default: + return nativeapi::DialogModality::None; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/display_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/display_c.cpp new file mode 100644 index 0000000..2ce8abd --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/display_c.cpp @@ -0,0 +1,235 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "display_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" +#include "geometry_c.h" +#include "../display.h" + +native_display_t native_display_create(void* display) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(display)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_create"); + return 0; + } +} + +native_display_id_t native_display_get_id(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return 0; + } + try { + return self->GetId(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_id"); + return 0; + } +} + +char* native_display_get_name(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetName()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_name"); + return nullptr; + } +} + +native_point_t native_display_get_position(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + native_point_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetPosition(); + return to_c_point(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_position"); + native_point_t result = {}; + return result; + } +} + +native_size_t native_display_get_size(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + native_size_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetSize(); + return to_c_size(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_size"); + native_size_t result = {}; + return result; + } +} + +native_rectangle_t native_display_get_work_area(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + native_rectangle_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetWorkArea(); + return to_c_rectangle(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_work_area"); + native_rectangle_t result = {}; + return result; + } +} + +double native_display_get_scale_factor(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return 0; + } + try { + return self->GetScaleFactor(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_scale_factor"); + return 0; + } +} + +bool native_display_is_primary(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return false; + } + try { + return self->IsPrimary(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_is_primary"); + return false; + } +} + +native_display_orientation_t native_display_get_orientation(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return (native_display_orientation_t)NATIVE_DISPLAY_ORIENTATION_PORTRAIT; + } + try { + return to_c_display_orientation(self->GetOrientation()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_orientation"); + return (native_display_orientation_t)NATIVE_DISPLAY_ORIENTATION_PORTRAIT; + } +} + +int native_display_get_refresh_rate(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return 0; + } + try { + return self->GetRefreshRate(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_refresh_rate"); + return 0; + } +} + +int native_display_get_bit_depth(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return 0; + } + try { + return self->GetBitDepth(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_get_bit_depth"); + return 0; + } +} + +void* native_display_get_native_object(native_display_t display) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(display); + if (!self) { + return nullptr; + } + return self->GetNativeObject(); +} + +void native_display_free(native_display_t display) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(display); +} + +void native_display_list_free(native_display_list_t* list) { + if (!list || !list->displays) { + return; + } + for (long i = 0; i < list->count; ++i) { + nativeapi::HandleTable::GetInstance().Release(list->displays[i]); + } + delete[] list->displays; + list->displays = nullptr; + list->count = 0; +} + +void native_display_list_release(native_display_list_t* list) { + if (!list) { + return; + } + delete[] list->displays; + list->displays = nullptr; + list->count = 0; +} + +bool to_c_display_event(const nativeapi::DisplayEvent& event, native_display_event_t* out) { + if (!out) { + return false; + } + *out = native_display_event_t{}; + out->display = nativeapi::HandleTable::GetInstance().Insert(event.GetDisplay()); + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_DISPLAY_EVENT_TYPE_ADDED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_DISPLAY_EVENT_TYPE_REMOVED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_DISPLAY_EVENT_TYPE_CHANGED; + (void)typed; + return true; + } + return false; +} + +void free_c_display_event(native_display_event_t* value) { + if (!value) { + return; + } + nativeapi::HandleTable::GetInstance().Release(value->display); + value->display = 0; +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/display_c.h b/packages/cnativeapi/cxx_impl/src/capi/display_c.h new file mode 100644 index 0000000..dce0eb2 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/display_c.h @@ -0,0 +1,174 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "geometry_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int native_display_id_t; + +typedef enum { + NATIVE_DISPLAY_ORIENTATION_PORTRAIT = 0, + NATIVE_DISPLAY_ORIENTATION_LANDSCAPE = 90, + NATIVE_DISPLAY_ORIENTATION_PORTRAIT_FLIPPED = 180, + NATIVE_DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED = 270, +} native_display_orientation_t; + +/// Opaque Display handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_DISPLAY rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_display_t; + +/// Never refers to a live Display. +#define NATIVE_INVALID_DISPLAY ((native_display_t)0) + +/// Owning list of Display handles. +typedef struct { + native_display_t* displays; + long count; +} native_display_list_t; + +/// Which concrete DisplayEvent arrived. +typedef enum { + NATIVE_DISPLAY_EVENT_TYPE_ADDED = 0, + NATIVE_DISPLAY_EVENT_TYPE_REMOVED = 1, + NATIVE_DISPLAY_EVENT_TYPE_CHANGED = 2, +} native_display_event_type_t; + +/// One DisplayEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_display_event_type_t type; + native_display_t display; +} native_display_event_t; + +typedef void (*native_display_event_callback_t)(const native_display_event_t* event, void* user_data); + +/// Creates a Display instance; release it with native_display_free(). +FFI_PLUGIN_EXPORT +native_display_t native_display_create(void* display); + +FFI_PLUGIN_EXPORT +native_display_id_t native_display_get_id(native_display_t display); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_display_get_name(native_display_t display); + +FFI_PLUGIN_EXPORT +native_point_t native_display_get_position(native_display_t display); + +FFI_PLUGIN_EXPORT +native_size_t native_display_get_size(native_display_t display); + +FFI_PLUGIN_EXPORT +native_rectangle_t native_display_get_work_area(native_display_t display); + +FFI_PLUGIN_EXPORT +double native_display_get_scale_factor(native_display_t display); + +FFI_PLUGIN_EXPORT +bool native_display_is_primary(native_display_t display); + +FFI_PLUGIN_EXPORT +native_display_orientation_t native_display_get_orientation(native_display_t display); + +FFI_PLUGIN_EXPORT +int native_display_get_refresh_rate(native_display_t display); + +FFI_PLUGIN_EXPORT +int native_display_get_bit_depth(native_display_t display); + +/// Platform-specific native object (NSScreen*, HMONITOR, ...). +FFI_PLUGIN_EXPORT +void* native_display_get_native_object(native_display_t display); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_display_free(native_display_t display); + +/// Frees the array and releases every handle it contains. +FFI_PLUGIN_EXPORT +void native_display_list_free(native_display_list_t* list); + +/// Frees only the array; the caller takes over the handles. +FFI_PLUGIN_EXPORT +void native_display_list_release(native_display_list_t* list); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class DisplayEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_display_event(const nativeapi::DisplayEvent& event, native_display_event_t* out); +/// Releases everything to_c_display_event() allocated. +void free_c_display_event(native_display_event_t* value); + +#endif + +#ifdef __cplusplus +#include "../display.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_display_orientation_t to_c_display_orientation(nativeapi::DisplayOrientation value); +inline nativeapi::DisplayOrientation to_cpp_display_orientation(native_display_orientation_t value); + +inline native_display_orientation_t to_c_display_orientation(nativeapi::DisplayOrientation value) { + switch (value) { + case nativeapi::DisplayOrientation::kPortrait: + return NATIVE_DISPLAY_ORIENTATION_PORTRAIT; + case nativeapi::DisplayOrientation::kLandscape: + return NATIVE_DISPLAY_ORIENTATION_LANDSCAPE; + case nativeapi::DisplayOrientation::kPortraitFlipped: + return NATIVE_DISPLAY_ORIENTATION_PORTRAIT_FLIPPED; + case nativeapi::DisplayOrientation::kLandscapeFlipped: + return NATIVE_DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED; + default: + return NATIVE_DISPLAY_ORIENTATION_PORTRAIT; + } +} + +inline nativeapi::DisplayOrientation to_cpp_display_orientation(native_display_orientation_t value) { + switch (value) { + case NATIVE_DISPLAY_ORIENTATION_PORTRAIT: + return nativeapi::DisplayOrientation::kPortrait; + case NATIVE_DISPLAY_ORIENTATION_LANDSCAPE: + return nativeapi::DisplayOrientation::kLandscape; + case NATIVE_DISPLAY_ORIENTATION_PORTRAIT_FLIPPED: + return nativeapi::DisplayOrientation::kPortraitFlipped; + case NATIVE_DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: + return nativeapi::DisplayOrientation::kLandscapeFlipped; + default: + return nativeapi::DisplayOrientation::kPortrait; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/display_manager_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/display_manager_c.cpp new file mode 100644 index 0000000..6a163be --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/display_manager_c.cpp @@ -0,0 +1,91 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "display_manager_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" +#include "geometry_c.h" +#include "../display.h" +#include "display_c.h" +#include "../display_manager.h" + +native_display_list_t native_display_manager_get_all(void) { + try { + const auto items = nativeapi::DisplayManager::GetInstance().GetAll(); + native_display_list_t list = {}; + if (items.empty()) { + return list; + } + list.displays = new (std::nothrow) native_display_t[items.size()]; + if (!list.displays) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.displays[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_manager_get_all"); + native_display_list_t empty = {}; + return empty; + } +} + +native_display_t native_display_manager_get_primary(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::DisplayManager::GetInstance().GetPrimary()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_manager_get_primary"); + return 0; + } +} + +native_point_t native_display_manager_get_cursor_position(void) { + try { + const auto cpp_result = nativeapi::DisplayManager::GetInstance().GetCursorPosition(); + return to_c_point(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_display_manager_get_cursor_position"); + native_point_t result = {}; + return result; + } +} + +native_listener_id_t native_display_manager_add_listener(native_display_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + try { + return static_cast(nativeapi::DisplayManager::GetInstance().AddListener( + [callback, user_data](const nativeapi::DisplayEvent& event) { + native_display_event_t c_event = {}; + if (!to_c_display_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_display_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_display_manager_remove_listener(native_listener_id_t listener_id) { + try { + return nativeapi::DisplayManager::GetInstance().RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/display_manager_c.h b/packages/cnativeapi/cxx_impl/src/capi/display_manager_c.h new file mode 100644 index 0000000..58db9b6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/display_manager_c.h @@ -0,0 +1,44 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "display_c.h" +#include "geometry_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +FFI_PLUGIN_EXPORT +native_display_list_t native_display_manager_get_all(void); + +/// Caller owns the returned handle; release it with native_display_free(). +FFI_PLUGIN_EXPORT +native_display_t native_display_manager_get_primary(void); + +FFI_PLUGIN_EXPORT +native_point_t native_display_manager_get_cursor_position(void); + +/// Registers @p callback for every DisplayEvent this DisplayManager emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_display_manager_add_listener(native_display_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_display_manager_remove_listener(native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/geometry_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/geometry_c.cpp new file mode 100644 index 0000000..073ae0c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/geometry_c.cpp @@ -0,0 +1,17 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "geometry_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" + diff --git a/packages/cnativeapi/cxx_impl/src/capi/geometry_c.h b/packages/cnativeapi/cxx_impl/src/capi/geometry_c.h new file mode 100644 index 0000000..9d5c21c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/geometry_c.h @@ -0,0 +1,101 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + double x; + double y; +} native_point_t; + +typedef struct { + double width; + double height; +} native_size_t; + +typedef struct { + double x; + double y; + double width; + double height; +} native_rectangle_t; + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../foundation/geometry.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_point_t to_c_point(const nativeapi::Point& value); +inline nativeapi::Point to_cpp_point(const native_point_t& value); +inline native_size_t to_c_size(const nativeapi::Size& value); +inline nativeapi::Size to_cpp_size(const native_size_t& value); +inline native_rectangle_t to_c_rectangle(const nativeapi::Rectangle& value); +inline nativeapi::Rectangle to_cpp_rectangle(const native_rectangle_t& value); + +inline native_point_t to_c_point(const nativeapi::Point& value) { + native_point_t result = {}; + result.x = value.x; + result.y = value.y; + return result; +} + +inline nativeapi::Point to_cpp_point(const native_point_t& value) { + nativeapi::Point result = {}; + result.x = value.x; + result.y = value.y; + return result; +} + +inline native_size_t to_c_size(const nativeapi::Size& value) { + native_size_t result = {}; + result.width = value.width; + result.height = value.height; + return result; +} + +inline nativeapi::Size to_cpp_size(const native_size_t& value) { + nativeapi::Size result = {}; + result.width = value.width; + result.height = value.height; + return result; +} + +inline native_rectangle_t to_c_rectangle(const nativeapi::Rectangle& value) { + native_rectangle_t result = {}; + result.x = value.x; + result.y = value.y; + result.width = value.width; + result.height = value.height; + return result; +} + +inline nativeapi::Rectangle to_cpp_rectangle(const native_rectangle_t& value) { + nativeapi::Rectangle result = {}; + result.x = value.x; + result.y = value.y; + result.width = value.width; + result.height = value.height; + return result; +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/image_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/image_c.cpp new file mode 100644 index 0000000..bfa965e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/image_c.cpp @@ -0,0 +1,106 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "image_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" +#include "geometry_c.h" +#include "../image.h" + +native_image_t native_image_from_file(const char* file_path) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::Image::FromFile(std::string(file_path ? file_path : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_image_from_file"); + return 0; + } +} + +native_image_t native_image_from_base64(const char* base64_data) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::Image::FromBase64(std::string(base64_data ? base64_data : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_image_from_base64"); + return 0; + } +} + +native_size_t native_image_get_size(native_image_t image) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(image); + if (!self) { + native_size_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetSize(); + return to_c_size(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_image_get_size"); + native_size_t result = {}; + return result; + } +} + +char* native_image_get_format(native_image_t image) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(image); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetFormat()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_image_get_format"); + return nullptr; + } +} + +char* native_image_to_base64(native_image_t image) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(image); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->ToBase64()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_image_to_base64"); + return nullptr; + } +} + +bool native_image_save_to_file(native_image_t image, const char* file_path) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(image); + if (!self) { + return false; + } + try { + return self->SaveToFile(std::string(file_path ? file_path : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_image_save_to_file"); + return false; + } +} + +void* native_image_get_native_object(native_image_t image) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(image); + if (!self) { + return nullptr; + } + return self->GetNativeObject(); +} + +void native_image_free(native_image_t image) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(image); +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/image_c.h b/packages/cnativeapi/cxx_impl/src/capi/image_c.h new file mode 100644 index 0000000..04f0a89 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/image_c.h @@ -0,0 +1,66 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "geometry_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque Image handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_IMAGE rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_image_t; + +/// Never refers to a live Image. +#define NATIVE_INVALID_IMAGE ((native_image_t)0) + +/// Caller owns the returned handle; release it with native_image_free(). +FFI_PLUGIN_EXPORT +native_image_t native_image_from_file(const char* file_path); + +/// Caller owns the returned handle; release it with native_image_free(). +FFI_PLUGIN_EXPORT +native_image_t native_image_from_base64(const char* base64_data); + +FFI_PLUGIN_EXPORT +native_size_t native_image_get_size(native_image_t image); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_image_get_format(native_image_t image); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_image_to_base64(native_image_t image); + +FFI_PLUGIN_EXPORT +bool native_image_save_to_file(native_image_t image, const char* file_path); + +/// Platform-specific native object (NSScreen*, HMONITOR, ...). +FFI_PLUGIN_EXPORT +void* native_image_get_native_object(native_image_t image); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_image_free(native_image_t image); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/keyboard_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/keyboard_c.cpp new file mode 100644 index 0000000..758a896 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/keyboard_c.cpp @@ -0,0 +1,75 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "keyboard_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/keyboard.h" + +char* native_keyboard_accelerator_to_string(native_keyboard_accelerator_t keyboard_accelerator) { + try { + const auto self = to_cpp_keyboard_accelerator(keyboard_accelerator); + return to_c_str(self.ToString()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_keyboard_accelerator_to_string"); + return nullptr; + } +} + +bool native_keyboard_accelerator_is_empty(native_keyboard_accelerator_t keyboard_accelerator) { + try { + const auto self = to_cpp_keyboard_accelerator(keyboard_accelerator); + return self.IsEmpty(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_keyboard_accelerator_is_empty"); + return false; + } +} + +void native_keyboard_accelerator_free(native_keyboard_accelerator_t* value) { + if (!value) { + return; + } + free_c_str(value->key); + value->key = nullptr; +} + +bool to_c_keyboard_event(const nativeapi::KeyboardEvent& event, native_keyboard_event_t* out) { + if (!out) { + return false; + } + *out = native_keyboard_event_t{}; + out->keycode = event.GetKeycode(); + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_KEYBOARD_EVENT_TYPE_KEY_PRESSED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_KEYBOARD_EVENT_TYPE_KEY_RELEASED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_KEYBOARD_EVENT_TYPE_MODIFIER_KEYS_CHANGED; + out->data.modifier_keys_changed.modifier_keys = typed->GetModifierKeys(); + return true; + } + return false; +} + +void free_c_keyboard_event(native_keyboard_event_t* value) { + if (!value) { + return; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/keyboard_c.h b/packages/cnativeapi/cxx_impl/src/capi/keyboard_c.h new file mode 100644 index 0000000..73e3c8f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/keyboard_c.h @@ -0,0 +1,164 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + NATIVE_MODIFIER_KEY_NONE = 0, + NATIVE_MODIFIER_KEY_SHIFT = 1, + NATIVE_MODIFIER_KEY_CTRL = 2, + NATIVE_MODIFIER_KEY_ALT = 4, + NATIVE_MODIFIER_KEY_META = 8, + NATIVE_MODIFIER_KEY_FN = 16, + NATIVE_MODIFIER_KEY_CAPS_LOCK = 32, + NATIVE_MODIFIER_KEY_NUM_LOCK = 64, + NATIVE_MODIFIER_KEY_SCROLL_LOCK = 128, +} native_modifier_key_t; + +typedef struct { + native_modifier_key_t modifiers; + char* key; +} native_keyboard_accelerator_t; + +/// Which concrete KeyboardEvent arrived. +typedef enum { + NATIVE_KEYBOARD_EVENT_TYPE_KEY_PRESSED = 0, + NATIVE_KEYBOARD_EVENT_TYPE_KEY_RELEASED = 1, + NATIVE_KEYBOARD_EVENT_TYPE_MODIFIER_KEYS_CHANGED = 2, +} native_keyboard_event_type_t; + +/// One KeyboardEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_keyboard_event_type_t type; + int keycode; + union { + struct { + unsigned int modifier_keys; + } modifier_keys_changed; + } data; +} native_keyboard_event_t; + +typedef void (*native_keyboard_event_callback_t)(const native_keyboard_event_t* event, void* user_data); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_keyboard_accelerator_to_string(native_keyboard_accelerator_t keyboard_accelerator); + +FFI_PLUGIN_EXPORT +bool native_keyboard_accelerator_is_empty(native_keyboard_accelerator_t keyboard_accelerator); + +/// Frees everything the struct owns. +FFI_PLUGIN_EXPORT +void native_keyboard_accelerator_free(native_keyboard_accelerator_t* value); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class KeyboardEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_keyboard_event(const nativeapi::KeyboardEvent& event, native_keyboard_event_t* out); +/// Releases everything to_c_keyboard_event() allocated. +void free_c_keyboard_event(native_keyboard_event_t* value); + +#endif + +#ifdef __cplusplus +#include "../foundation/keyboard.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_modifier_key_t to_c_modifier_key(nativeapi::ModifierKey value); +inline nativeapi::ModifierKey to_cpp_modifier_key(native_modifier_key_t value); +inline native_keyboard_accelerator_t to_c_keyboard_accelerator(const nativeapi::KeyboardAccelerator& value); +inline nativeapi::KeyboardAccelerator to_cpp_keyboard_accelerator(const native_keyboard_accelerator_t& value); + +inline native_modifier_key_t to_c_modifier_key(nativeapi::ModifierKey value) { + switch (value) { + case nativeapi::ModifierKey::None: + return NATIVE_MODIFIER_KEY_NONE; + case nativeapi::ModifierKey::Shift: + return NATIVE_MODIFIER_KEY_SHIFT; + case nativeapi::ModifierKey::Ctrl: + return NATIVE_MODIFIER_KEY_CTRL; + case nativeapi::ModifierKey::Alt: + return NATIVE_MODIFIER_KEY_ALT; + case nativeapi::ModifierKey::Meta: + return NATIVE_MODIFIER_KEY_META; + case nativeapi::ModifierKey::Fn: + return NATIVE_MODIFIER_KEY_FN; + case nativeapi::ModifierKey::CapsLock: + return NATIVE_MODIFIER_KEY_CAPS_LOCK; + case nativeapi::ModifierKey::NumLock: + return NATIVE_MODIFIER_KEY_NUM_LOCK; + case nativeapi::ModifierKey::ScrollLock: + return NATIVE_MODIFIER_KEY_SCROLL_LOCK; + default: + return NATIVE_MODIFIER_KEY_NONE; + } +} + +inline nativeapi::ModifierKey to_cpp_modifier_key(native_modifier_key_t value) { + switch (value) { + case NATIVE_MODIFIER_KEY_NONE: + return nativeapi::ModifierKey::None; + case NATIVE_MODIFIER_KEY_SHIFT: + return nativeapi::ModifierKey::Shift; + case NATIVE_MODIFIER_KEY_CTRL: + return nativeapi::ModifierKey::Ctrl; + case NATIVE_MODIFIER_KEY_ALT: + return nativeapi::ModifierKey::Alt; + case NATIVE_MODIFIER_KEY_META: + return nativeapi::ModifierKey::Meta; + case NATIVE_MODIFIER_KEY_FN: + return nativeapi::ModifierKey::Fn; + case NATIVE_MODIFIER_KEY_CAPS_LOCK: + return nativeapi::ModifierKey::CapsLock; + case NATIVE_MODIFIER_KEY_NUM_LOCK: + return nativeapi::ModifierKey::NumLock; + case NATIVE_MODIFIER_KEY_SCROLL_LOCK: + return nativeapi::ModifierKey::ScrollLock; + default: + return nativeapi::ModifierKey::None; + } +} + +inline native_keyboard_accelerator_t to_c_keyboard_accelerator(const nativeapi::KeyboardAccelerator& value) { + native_keyboard_accelerator_t result = {}; + result.modifiers = to_c_modifier_key(value.modifiers); + result.key = to_c_str(value.key); + return result; +} + +inline nativeapi::KeyboardAccelerator to_cpp_keyboard_accelerator(const native_keyboard_accelerator_t& value) { + nativeapi::KeyboardAccelerator result = {}; + result.modifiers = to_cpp_modifier_key(value.modifiers); + result.key = value.key ? value.key : ""; + return result; +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.cpp new file mode 100644 index 0000000..dbadcd8 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.cpp @@ -0,0 +1,111 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "keyboard_monitor_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/keyboard.h" +#include "keyboard_c.h" +#include "../keyboard_monitor.h" + +native_keyboard_monitor_t native_keyboard_monitor_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_keyboard_monitor_create"); + return 0; + } +} + +void native_keyboard_monitor_start(native_keyboard_monitor_t keyboard_monitor) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(keyboard_monitor); + if (!self) { + return; + } + try { + self->Start(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_keyboard_monitor_start"); + return; + } +} + +void native_keyboard_monitor_stop(native_keyboard_monitor_t keyboard_monitor) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(keyboard_monitor); + if (!self) { + return; + } + try { + self->Stop(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_keyboard_monitor_stop"); + return; + } +} + +bool native_keyboard_monitor_is_monitoring(native_keyboard_monitor_t keyboard_monitor) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(keyboard_monitor); + if (!self) { + return false; + } + try { + return self->IsMonitoring(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_keyboard_monitor_is_monitoring"); + return false; + } +} + +void native_keyboard_monitor_free(native_keyboard_monitor_t keyboard_monitor) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(keyboard_monitor); +} + +native_listener_id_t native_keyboard_monitor_add_listener(native_keyboard_monitor_t keyboard_monitor, native_keyboard_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + auto self = nativeapi::HandleTable::GetInstance().Resolve(keyboard_monitor); + if (!self) { + return 0; + } + try { + return static_cast(self->AddListener( + [callback, user_data](const nativeapi::KeyboardEvent& event) { + native_keyboard_event_t c_event = {}; + if (!to_c_keyboard_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_keyboard_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_keyboard_monitor_remove_listener(native_keyboard_monitor_t keyboard_monitor, native_listener_id_t listener_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(keyboard_monitor); + if (!self) { + return false; + } + try { + return self->RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.h b/packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.h new file mode 100644 index 0000000..099a186 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/keyboard_monitor_c.h @@ -0,0 +1,62 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "keyboard_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque KeyboardMonitor handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_KEYBOARD_MONITOR rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_keyboard_monitor_t; + +/// Never refers to a live KeyboardMonitor. +#define NATIVE_INVALID_KEYBOARD_MONITOR ((native_keyboard_monitor_t)0) + +/// Creates a KeyboardMonitor instance; release it with native_keyboard_monitor_free(). +FFI_PLUGIN_EXPORT +native_keyboard_monitor_t native_keyboard_monitor_create(void); + +FFI_PLUGIN_EXPORT +void native_keyboard_monitor_start(native_keyboard_monitor_t keyboard_monitor); + +FFI_PLUGIN_EXPORT +void native_keyboard_monitor_stop(native_keyboard_monitor_t keyboard_monitor); + +FFI_PLUGIN_EXPORT +bool native_keyboard_monitor_is_monitoring(native_keyboard_monitor_t keyboard_monitor); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_keyboard_monitor_free(native_keyboard_monitor_t keyboard_monitor); + +/// Registers @p callback for every KeyboardEvent this KeyboardMonitor emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_keyboard_monitor_add_listener(native_keyboard_monitor_t keyboard_monitor, native_keyboard_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_keyboard_monitor_remove_listener(native_keyboard_monitor_t keyboard_monitor, native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.cpp new file mode 100644 index 0000000..eb8bcdf --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.cpp @@ -0,0 +1,185 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "launch_at_login_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../launch_at_login.h" + +native_launch_at_login_t native_launch_at_login_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_create"); + return 0; + } +} + +native_launch_at_login_t native_launch_at_login_create_with_id(const char* id) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(std::string(id ? id : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_create_with_id"); + return 0; + } +} + +native_launch_at_login_t native_launch_at_login_create_with_id_and_display_name(const char* id, const char* display_name) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(std::string(id ? id : ""), std::string(display_name ? display_name : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_create_with_id_and_display_name"); + return 0; + } +} + +bool native_launch_at_login_is_supported(void) { + try { + return nativeapi::LaunchAtLogin::IsSupported(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_is_supported"); + return false; + } +} + +char* native_launch_at_login_get_id(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetId()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_get_id"); + return nullptr; + } +} + +char* native_launch_at_login_get_display_name(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetDisplayName()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_get_display_name"); + return nullptr; + } +} + +bool native_launch_at_login_set_display_name(native_launch_at_login_t launch_at_login, const char* display_name) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return false; + } + try { + return self->SetDisplayName(std::string(display_name ? display_name : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_set_display_name"); + return false; + } +} + +bool native_launch_at_login_set_program(native_launch_at_login_t launch_at_login, const char* executable_path, native_string_list_t arguments) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return false; + } + try { + std::vector arguments_cpp; + for (long i = 0; i < arguments.count; ++i) { + arguments_cpp.emplace_back(arguments.items[i] ? arguments.items[i] : ""); + } + return self->SetProgram(std::string(executable_path ? executable_path : ""), arguments_cpp); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_set_program"); + return false; + } +} + +char* native_launch_at_login_get_executable_path(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetExecutablePath()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_get_executable_path"); + return nullptr; + } +} + +native_string_list_t native_launch_at_login_get_arguments(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + native_string_list_t empty = {}; + return empty; + } + try { + return to_c_string_list(self->GetArguments()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_get_arguments"); + native_string_list_t empty = {}; + return empty; + } +} + +bool native_launch_at_login_enable(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return false; + } + try { + return self->Enable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_enable"); + return false; + } +} + +bool native_launch_at_login_disable(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return false; + } + try { + return self->Disable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_disable"); + return false; + } +} + +bool native_launch_at_login_is_enabled(native_launch_at_login_t launch_at_login) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(launch_at_login); + if (!self) { + return false; + } + try { + return self->IsEnabled(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_launch_at_login_is_enabled"); + return false; + } +} + +void native_launch_at_login_free(native_launch_at_login_t launch_at_login) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(launch_at_login); +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.h b/packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.h new file mode 100644 index 0000000..8b23f80 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/launch_at_login_c.h @@ -0,0 +1,85 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "string_utils_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque LaunchAtLogin handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_LAUNCH_AT_LOGIN rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_launch_at_login_t; + +/// Never refers to a live LaunchAtLogin. +#define NATIVE_INVALID_LAUNCH_AT_LOGIN ((native_launch_at_login_t)0) + +/// Creates a LaunchAtLogin instance; release it with native_launch_at_login_free(). +FFI_PLUGIN_EXPORT +native_launch_at_login_t native_launch_at_login_create(void); + +/// Creates a LaunchAtLogin instance; release it with native_launch_at_login_free(). +FFI_PLUGIN_EXPORT +native_launch_at_login_t native_launch_at_login_create_with_id(const char* id); + +/// Creates a LaunchAtLogin instance; release it with native_launch_at_login_free(). +FFI_PLUGIN_EXPORT +native_launch_at_login_t native_launch_at_login_create_with_id_and_display_name(const char* id, const char* display_name); + +FFI_PLUGIN_EXPORT +bool native_launch_at_login_is_supported(void); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_launch_at_login_get_id(native_launch_at_login_t launch_at_login); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_launch_at_login_get_display_name(native_launch_at_login_t launch_at_login); + +FFI_PLUGIN_EXPORT +bool native_launch_at_login_set_display_name(native_launch_at_login_t launch_at_login, const char* display_name); + +FFI_PLUGIN_EXPORT +bool native_launch_at_login_set_program(native_launch_at_login_t launch_at_login, const char* executable_path, native_string_list_t arguments); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_launch_at_login_get_executable_path(native_launch_at_login_t launch_at_login); + +FFI_PLUGIN_EXPORT +native_string_list_t native_launch_at_login_get_arguments(native_launch_at_login_t launch_at_login); + +FFI_PLUGIN_EXPORT +bool native_launch_at_login_enable(native_launch_at_login_t launch_at_login); + +FFI_PLUGIN_EXPORT +bool native_launch_at_login_disable(native_launch_at_login_t launch_at_login); + +FFI_PLUGIN_EXPORT +bool native_launch_at_login_is_enabled(native_launch_at_login_t launch_at_login); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_launch_at_login_free(native_launch_at_login_t launch_at_login); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/menu_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/menu_c.cpp new file mode 100644 index 0000000..db54881 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/menu_c.cpp @@ -0,0 +1,706 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "menu_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/keyboard.h" +#include "keyboard_c.h" +#include "../placement.h" +#include "placement_c.h" +#include "../image.h" +#include "image_c.h" +#include "../positioning_strategy.h" +#include "positioning_strategy_c.h" +#include "../menu.h" + +native_menu_item_t native_menu_item_create_with_label_and_type(const char* label, native_menu_item_type_t type) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(std::string(label ? label : ""), to_cpp_menu_item_type(type))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_create_with_label_and_type"); + return 0; + } +} + +native_menu_item_t native_menu_item_create_with_native_item(void* native_item) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(native_item)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_create_with_native_item"); + return 0; + } +} + +native_menu_item_id_t native_menu_item_get_id(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return 0; + } + try { + return self->GetId(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_id"); + return 0; + } +} + +native_menu_item_type_t native_menu_item_get_type(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return (native_menu_item_type_t)NATIVE_MENU_ITEM_TYPE_NORMAL; + } + try { + return to_c_menu_item_type(self->GetType()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_type"); + return (native_menu_item_type_t)NATIVE_MENU_ITEM_TYPE_NORMAL; + } +} + +void native_menu_item_set_label(native_menu_item_t menu_item, const char* label) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + std::optional label_cpp; + if (label) { + label_cpp = std::string(label); + } + self->SetLabel(label_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_label"); + return; + } +} + +char* native_menu_item_get_label(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return nullptr; + } + try { + const auto cpp_result = self->GetLabel(); + return cpp_result ? to_c_str(*cpp_result) : nullptr; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_label"); + return nullptr; + } +} + +void native_menu_item_set_icon(native_menu_item_t menu_item, native_image_t image) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + auto image_cpp = nativeapi::HandleTable::GetInstance().Resolve(image); + self->SetIcon(image_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_icon"); + return; + } +} + +native_image_t native_menu_item_get_icon(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return 0; + } + try { + return nativeapi::HandleTable::GetInstance().Insert(self->GetIcon()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_icon"); + return 0; + } +} + +void native_menu_item_set_tooltip(native_menu_item_t menu_item, const char* tooltip) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + std::optional tooltip_cpp; + if (tooltip) { + tooltip_cpp = std::string(tooltip); + } + self->SetTooltip(tooltip_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_tooltip"); + return; + } +} + +char* native_menu_item_get_tooltip(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return nullptr; + } + try { + const auto cpp_result = self->GetTooltip(); + return cpp_result ? to_c_str(*cpp_result) : nullptr; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_tooltip"); + return nullptr; + } +} + +void native_menu_item_set_accelerator(native_menu_item_t menu_item, const native_keyboard_accelerator_t* accelerator) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + std::optional accelerator_cpp; + if (accelerator) { + accelerator_cpp = to_cpp_keyboard_accelerator(*accelerator); + } + self->SetAccelerator(accelerator_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_accelerator"); + return; + } +} + +native_keyboard_accelerator_t native_menu_item_get_accelerator(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + native_keyboard_accelerator_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetAccelerator(); + return to_c_keyboard_accelerator(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_accelerator"); + native_keyboard_accelerator_t result = {}; + return result; + } +} + +void native_menu_item_set_enabled(native_menu_item_t menu_item, bool enabled) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + self->SetEnabled(enabled); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_enabled"); + return; + } +} + +bool native_menu_item_is_enabled(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return false; + } + try { + return self->IsEnabled(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_is_enabled"); + return false; + } +} + +void native_menu_item_set_state(native_menu_item_t menu_item, native_menu_item_state_t state) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + self->SetState(to_cpp_menu_item_state(state)); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_state"); + return; + } +} + +native_menu_item_state_t native_menu_item_get_state(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return (native_menu_item_state_t)NATIVE_MENU_ITEM_STATE_UNCHECKED; + } + try { + return to_c_menu_item_state(self->GetState()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_state"); + return (native_menu_item_state_t)NATIVE_MENU_ITEM_STATE_UNCHECKED; + } +} + +void native_menu_item_set_radio_group(native_menu_item_t menu_item, int group_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + self->SetRadioGroup(group_id); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_radio_group"); + return; + } +} + +int native_menu_item_get_radio_group(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return 0; + } + try { + return self->GetRadioGroup(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_radio_group"); + return 0; + } +} + +void native_menu_item_set_submenu(native_menu_item_t menu_item, native_menu_t submenu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return; + } + try { + auto submenu_cpp = nativeapi::HandleTable::GetInstance().Resolve(submenu); + self->SetSubmenu(submenu_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_set_submenu"); + return; + } +} + +native_menu_t native_menu_item_get_submenu(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return 0; + } + try { + return nativeapi::HandleTable::GetInstance().Insert(self->GetSubmenu()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_item_get_submenu"); + return 0; + } +} + +void* native_menu_item_get_native_object(native_menu_item_t menu_item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return nullptr; + } + return self->GetNativeObject(); +} + +void native_menu_item_free(native_menu_item_t menu_item) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(menu_item); +} + +void native_menu_item_list_free(native_menu_item_list_t* list) { + if (!list || !list->menu_items) { + return; + } + for (long i = 0; i < list->count; ++i) { + nativeapi::HandleTable::GetInstance().Release(list->menu_items[i]); + } + delete[] list->menu_items; + list->menu_items = nullptr; + list->count = 0; +} + +void native_menu_item_list_release(native_menu_item_list_t* list) { + if (!list) { + return; + } + delete[] list->menu_items; + list->menu_items = nullptr; + list->count = 0; +} + +native_listener_id_t native_menu_item_add_listener(native_menu_item_t menu_item, native_menu_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return 0; + } + try { + return static_cast(self->AddListener( + [callback, user_data](const nativeapi::MenuEvent& event) { + native_menu_event_t c_event = {}; + if (!to_c_menu_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_menu_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_menu_item_remove_listener(native_menu_item_t menu_item, native_listener_id_t listener_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu_item); + if (!self) { + return false; + } + try { + return self->RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + +native_menu_t native_menu_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_create"); + return 0; + } +} + +native_menu_t native_menu_create_with_native_menu(void* native_menu) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(native_menu)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_create_with_native_menu"); + return 0; + } +} + +native_menu_id_t native_menu_get_id(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return 0; + } + try { + return self->GetId(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_get_id"); + return 0; + } +} + +void native_menu_add_item(native_menu_t menu, native_menu_item_t item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return; + } + try { + auto item_cpp = nativeapi::HandleTable::GetInstance().Resolve(item); + self->AddItem(item_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_add_item"); + return; + } +} + +void native_menu_insert_item(native_menu_t menu, unsigned long index, native_menu_item_t item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return; + } + try { + auto item_cpp = nativeapi::HandleTable::GetInstance().Resolve(item); + self->InsertItem(index, item_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_insert_item"); + return; + } +} + +bool native_menu_remove_item(native_menu_t menu, native_menu_item_t item) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return false; + } + try { + auto item_cpp = nativeapi::HandleTable::GetInstance().Resolve(item); + return self->RemoveItem(item_cpp); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_remove_item"); + return false; + } +} + +bool native_menu_remove_item_by_id(native_menu_t menu, native_menu_item_id_t item_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return false; + } + try { + return self->RemoveItemById(item_id); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_remove_item_by_id"); + return false; + } +} + +bool native_menu_remove_item_at(native_menu_t menu, unsigned long index) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return false; + } + try { + return self->RemoveItemAt(index); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_remove_item_at"); + return false; + } +} + +void native_menu_clear(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return; + } + try { + self->Clear(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_clear"); + return; + } +} + +void native_menu_add_separator(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return; + } + try { + self->AddSeparator(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_add_separator"); + return; + } +} + +void native_menu_insert_separator(native_menu_t menu, unsigned long index) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return; + } + try { + self->InsertSeparator(index); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_insert_separator"); + return; + } +} + +unsigned long native_menu_get_item_count(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return 0; + } + try { + return self->GetItemCount(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_get_item_count"); + return 0; + } +} + +native_menu_item_t native_menu_get_item_at(native_menu_t menu, unsigned long index) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return 0; + } + try { + return nativeapi::HandleTable::GetInstance().Insert(self->GetItemAt(index)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_get_item_at"); + return 0; + } +} + +native_menu_item_t native_menu_get_item_by_id(native_menu_t menu, native_menu_item_id_t item_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return 0; + } + try { + return nativeapi::HandleTable::GetInstance().Insert(self->GetItemById(item_id)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_get_item_by_id"); + return 0; + } +} + +native_menu_item_list_t native_menu_get_all_items(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + native_menu_item_list_t empty = {}; + return empty; + } + try { + const auto items = self->GetAllItems(); + native_menu_item_list_t list = {}; + if (items.empty()) { + return list; + } + list.menu_items = new (std::nothrow) native_menu_item_t[items.size()]; + if (!list.menu_items) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.menu_items[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_get_all_items"); + native_menu_item_list_t empty = {}; + return empty; + } +} + +bool native_menu_open(native_menu_t menu, native_positioning_strategy_t strategy, native_placement_t placement) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return false; + } + try { + auto strategy_cpp = nativeapi::HandleTable::GetInstance().Resolve(strategy); + if (!strategy_cpp) { + return false; + } + return self->Open(*strategy_cpp, to_cpp_placement(placement)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_open"); + return false; + } +} + +bool native_menu_close(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return false; + } + try { + return self->Close(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_menu_close"); + return false; + } +} + +void* native_menu_get_native_object(native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return nullptr; + } + return self->GetNativeObject(); +} + +void native_menu_free(native_menu_t menu) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(menu); +} + +native_listener_id_t native_menu_add_listener(native_menu_t menu, native_menu_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return 0; + } + try { + return static_cast(self->AddListener( + [callback, user_data](const nativeapi::MenuEvent& event) { + native_menu_event_t c_event = {}; + if (!to_c_menu_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_menu_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_menu_remove_listener(native_menu_t menu, native_listener_id_t listener_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(menu); + if (!self) { + return false; + } + try { + return self->RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + +bool to_c_menu_event(const nativeapi::MenuEvent& event, native_menu_event_t* out) { + if (!out) { + return false; + } + *out = native_menu_event_t{}; + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_MENU_EVENT_TYPE_OPENED; + out->data.opened.menu_id = typed->GetMenuId(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_MENU_EVENT_TYPE_CLOSED; + out->data.closed.menu_id = typed->GetMenuId(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED; + out->data.item_clicked.item_id = typed->GetItemId(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_OPENED; + out->data.item_submenu_opened.item_id = typed->GetItemId(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_CLOSED; + out->data.item_submenu_closed.item_id = typed->GetItemId(); + return true; + } + return false; +} + +void free_c_menu_event(native_menu_event_t* value) { + if (!value) { + return; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/menu_c.h b/packages/cnativeapi/cxx_impl/src/capi/menu_c.h new file mode 100644 index 0000000..f08340f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/menu_c.h @@ -0,0 +1,360 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "image_c.h" +#include "keyboard_c.h" +#include "placement_c.h" +#include "positioning_strategy_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int native_menu_id_t; + +typedef unsigned int native_menu_item_id_t; + +typedef enum { + NATIVE_MENU_ITEM_TYPE_NORMAL = 0, + NATIVE_MENU_ITEM_TYPE_CHECKBOX = 1, + NATIVE_MENU_ITEM_TYPE_RADIO = 2, + NATIVE_MENU_ITEM_TYPE_SEPARATOR = 3, + NATIVE_MENU_ITEM_TYPE_SUBMENU = 4, +} native_menu_item_type_t; + +typedef enum { + NATIVE_MENU_ITEM_STATE_UNCHECKED = 0, + NATIVE_MENU_ITEM_STATE_CHECKED = 1, + NATIVE_MENU_ITEM_STATE_MIXED = 2, +} native_menu_item_state_t; + +/// Opaque MenuItem handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_MENU_ITEM rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_menu_item_t; + +/// Never refers to a live MenuItem. +#define NATIVE_INVALID_MENU_ITEM ((native_menu_item_t)0) + +/// Owning list of MenuItem handles. +typedef struct { + native_menu_item_t* menu_items; + long count; +} native_menu_item_list_t; + +/// Opaque Menu handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_MENU rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_menu_t; + +/// Never refers to a live Menu. +#define NATIVE_INVALID_MENU ((native_menu_t)0) + +/// Which concrete MenuEvent arrived. +typedef enum { + NATIVE_MENU_EVENT_TYPE_OPENED = 0, + NATIVE_MENU_EVENT_TYPE_CLOSED = 1, + NATIVE_MENU_EVENT_TYPE_ITEM_CLICKED = 2, + NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_OPENED = 3, + NATIVE_MENU_EVENT_TYPE_ITEM_SUBMENU_CLOSED = 4, +} native_menu_event_type_t; + +/// One MenuEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_menu_event_type_t type; + union { + struct { + native_menu_id_t menu_id; + } opened; + struct { + native_menu_id_t menu_id; + } closed; + struct { + native_menu_item_id_t item_id; + } item_clicked; + struct { + native_menu_item_id_t item_id; + } item_submenu_opened; + struct { + native_menu_item_id_t item_id; + } item_submenu_closed; + } data; +} native_menu_event_t; + +typedef void (*native_menu_event_callback_t)(const native_menu_event_t* event, void* user_data); + +/// Creates a MenuItem instance; release it with native_menu_item_free(). +FFI_PLUGIN_EXPORT +native_menu_item_t native_menu_item_create_with_label_and_type(const char* label, native_menu_item_type_t type); + +/// Creates a MenuItem instance; release it with native_menu_item_free(). +FFI_PLUGIN_EXPORT +native_menu_item_t native_menu_item_create_with_native_item(void* native_item); + +FFI_PLUGIN_EXPORT +native_menu_item_id_t native_menu_item_get_id(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +native_menu_item_type_t native_menu_item_get_type(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_label(native_menu_item_t menu_item, const char* label); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_menu_item_get_label(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_icon(native_menu_item_t menu_item, native_image_t image); + +/// Caller owns the returned handle; release it with native_image_free(). +FFI_PLUGIN_EXPORT +native_image_t native_menu_item_get_icon(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_tooltip(native_menu_item_t menu_item, const char* tooltip); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_menu_item_get_tooltip(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_accelerator(native_menu_item_t menu_item, const native_keyboard_accelerator_t* accelerator); + +FFI_PLUGIN_EXPORT +native_keyboard_accelerator_t native_menu_item_get_accelerator(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_enabled(native_menu_item_t menu_item, bool enabled); + +FFI_PLUGIN_EXPORT +bool native_menu_item_is_enabled(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_state(native_menu_item_t menu_item, native_menu_item_state_t state); + +FFI_PLUGIN_EXPORT +native_menu_item_state_t native_menu_item_get_state(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_radio_group(native_menu_item_t menu_item, int group_id); + +FFI_PLUGIN_EXPORT +int native_menu_item_get_radio_group(native_menu_item_t menu_item); + +FFI_PLUGIN_EXPORT +void native_menu_item_set_submenu(native_menu_item_t menu_item, native_menu_t submenu); + +/// Caller owns the returned handle; release it with native_menu_free(). +FFI_PLUGIN_EXPORT +native_menu_t native_menu_item_get_submenu(native_menu_item_t menu_item); + +/// Platform-specific native object (NSScreen*, HMONITOR, ...). +FFI_PLUGIN_EXPORT +void* native_menu_item_get_native_object(native_menu_item_t menu_item); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_menu_item_free(native_menu_item_t menu_item); + +/// Frees the array and releases every handle it contains. +FFI_PLUGIN_EXPORT +void native_menu_item_list_free(native_menu_item_list_t* list); + +/// Frees only the array; the caller takes over the handles. +FFI_PLUGIN_EXPORT +void native_menu_item_list_release(native_menu_item_list_t* list); + +/// Registers @p callback for every MenuEvent this MenuItem emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_menu_item_add_listener(native_menu_item_t menu_item, native_menu_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_menu_item_remove_listener(native_menu_item_t menu_item, native_listener_id_t listener_id); + +/// Creates a Menu instance; release it with native_menu_free(). +FFI_PLUGIN_EXPORT +native_menu_t native_menu_create(void); + +/// Creates a Menu instance; release it with native_menu_free(). +FFI_PLUGIN_EXPORT +native_menu_t native_menu_create_with_native_menu(void* native_menu); + +FFI_PLUGIN_EXPORT +native_menu_id_t native_menu_get_id(native_menu_t menu); + +FFI_PLUGIN_EXPORT +void native_menu_add_item(native_menu_t menu, native_menu_item_t item); + +FFI_PLUGIN_EXPORT +void native_menu_insert_item(native_menu_t menu, unsigned long index, native_menu_item_t item); + +FFI_PLUGIN_EXPORT +bool native_menu_remove_item(native_menu_t menu, native_menu_item_t item); + +FFI_PLUGIN_EXPORT +bool native_menu_remove_item_by_id(native_menu_t menu, native_menu_item_id_t item_id); + +FFI_PLUGIN_EXPORT +bool native_menu_remove_item_at(native_menu_t menu, unsigned long index); + +FFI_PLUGIN_EXPORT +void native_menu_clear(native_menu_t menu); + +FFI_PLUGIN_EXPORT +void native_menu_add_separator(native_menu_t menu); + +FFI_PLUGIN_EXPORT +void native_menu_insert_separator(native_menu_t menu, unsigned long index); + +FFI_PLUGIN_EXPORT +unsigned long native_menu_get_item_count(native_menu_t menu); + +/// Caller owns the returned handle; release it with native_menu_item_free(). +FFI_PLUGIN_EXPORT +native_menu_item_t native_menu_get_item_at(native_menu_t menu, unsigned long index); + +/// Caller owns the returned handle; release it with native_menu_item_free(). +FFI_PLUGIN_EXPORT +native_menu_item_t native_menu_get_item_by_id(native_menu_t menu, native_menu_item_id_t item_id); + +FFI_PLUGIN_EXPORT +native_menu_item_list_t native_menu_get_all_items(native_menu_t menu); + +FFI_PLUGIN_EXPORT +bool native_menu_open(native_menu_t menu, native_positioning_strategy_t strategy, native_placement_t placement); + +FFI_PLUGIN_EXPORT +bool native_menu_close(native_menu_t menu); + +/// Platform-specific native object (NSScreen*, HMONITOR, ...). +FFI_PLUGIN_EXPORT +void* native_menu_get_native_object(native_menu_t menu); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_menu_free(native_menu_t menu); + +/// Registers @p callback for every MenuEvent this Menu emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_menu_add_listener(native_menu_t menu, native_menu_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_menu_remove_listener(native_menu_t menu, native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class MenuEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_menu_event(const nativeapi::MenuEvent& event, native_menu_event_t* out); +/// Releases everything to_c_menu_event() allocated. +void free_c_menu_event(native_menu_event_t* value); + +#endif + +#ifdef __cplusplus +#include "../menu.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_menu_item_type_t to_c_menu_item_type(nativeapi::MenuItemType value); +inline nativeapi::MenuItemType to_cpp_menu_item_type(native_menu_item_type_t value); +inline native_menu_item_state_t to_c_menu_item_state(nativeapi::MenuItemState value); +inline nativeapi::MenuItemState to_cpp_menu_item_state(native_menu_item_state_t value); + +inline native_menu_item_type_t to_c_menu_item_type(nativeapi::MenuItemType value) { + switch (value) { + case nativeapi::MenuItemType::Normal: + return NATIVE_MENU_ITEM_TYPE_NORMAL; + case nativeapi::MenuItemType::Checkbox: + return NATIVE_MENU_ITEM_TYPE_CHECKBOX; + case nativeapi::MenuItemType::Radio: + return NATIVE_MENU_ITEM_TYPE_RADIO; + case nativeapi::MenuItemType::Separator: + return NATIVE_MENU_ITEM_TYPE_SEPARATOR; + case nativeapi::MenuItemType::Submenu: + return NATIVE_MENU_ITEM_TYPE_SUBMENU; + default: + return NATIVE_MENU_ITEM_TYPE_NORMAL; + } +} + +inline nativeapi::MenuItemType to_cpp_menu_item_type(native_menu_item_type_t value) { + switch (value) { + case NATIVE_MENU_ITEM_TYPE_NORMAL: + return nativeapi::MenuItemType::Normal; + case NATIVE_MENU_ITEM_TYPE_CHECKBOX: + return nativeapi::MenuItemType::Checkbox; + case NATIVE_MENU_ITEM_TYPE_RADIO: + return nativeapi::MenuItemType::Radio; + case NATIVE_MENU_ITEM_TYPE_SEPARATOR: + return nativeapi::MenuItemType::Separator; + case NATIVE_MENU_ITEM_TYPE_SUBMENU: + return nativeapi::MenuItemType::Submenu; + default: + return nativeapi::MenuItemType::Normal; + } +} + +inline native_menu_item_state_t to_c_menu_item_state(nativeapi::MenuItemState value) { + switch (value) { + case nativeapi::MenuItemState::Unchecked: + return NATIVE_MENU_ITEM_STATE_UNCHECKED; + case nativeapi::MenuItemState::Checked: + return NATIVE_MENU_ITEM_STATE_CHECKED; + case nativeapi::MenuItemState::Mixed: + return NATIVE_MENU_ITEM_STATE_MIXED; + default: + return NATIVE_MENU_ITEM_STATE_UNCHECKED; + } +} + +inline nativeapi::MenuItemState to_cpp_menu_item_state(native_menu_item_state_t value) { + switch (value) { + case NATIVE_MENU_ITEM_STATE_UNCHECKED: + return nativeapi::MenuItemState::Unchecked; + case NATIVE_MENU_ITEM_STATE_CHECKED: + return nativeapi::MenuItemState::Checked; + case NATIVE_MENU_ITEM_STATE_MIXED: + return nativeapi::MenuItemState::Mixed; + default: + return nativeapi::MenuItemState::Unchecked; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.cpp new file mode 100644 index 0000000..9834a8d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.cpp @@ -0,0 +1,142 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "message_dialog_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../dialog.h" +#include "dialog_c.h" +#include "../message_dialog.h" + +native_message_dialog_t native_message_dialog_create(const char* title, const char* message) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(std::string(title ? title : ""), std::string(message ? message : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_create"); + return 0; + } +} + +void native_message_dialog_set_title(native_message_dialog_t message_dialog, const char* title) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return; + } + try { + self->SetTitle(std::string(title ? title : "")); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_set_title"); + return; + } +} + +char* native_message_dialog_get_title(native_message_dialog_t message_dialog) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetTitle()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_get_title"); + return nullptr; + } +} + +void native_message_dialog_set_message(native_message_dialog_t message_dialog, const char* message) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return; + } + try { + self->SetMessage(std::string(message ? message : "")); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_set_message"); + return; + } +} + +char* native_message_dialog_get_message(native_message_dialog_t message_dialog) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetMessage()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_get_message"); + return nullptr; + } +} + +native_dialog_modality_t native_message_dialog_get_modality(native_message_dialog_t message_dialog) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return (native_dialog_modality_t)0; + } + try { + return to_c_dialog_modality(self->GetModality()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_get_modality"); + return (native_dialog_modality_t)0; + } +} + +void native_message_dialog_set_modality(native_message_dialog_t message_dialog, native_dialog_modality_t modality) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return; + } + try { + self->SetModality(to_cpp_dialog_modality(modality)); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_set_modality"); + return; + } +} + +bool native_message_dialog_open(native_message_dialog_t message_dialog) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return false; + } + try { + return self->Open(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_open"); + return false; + } +} + +bool native_message_dialog_close(native_message_dialog_t message_dialog) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(message_dialog); + if (!self) { + return false; + } + try { + return self->Close(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_message_dialog_close"); + return false; + } +} + +void native_message_dialog_free(native_message_dialog_t message_dialog) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(message_dialog); +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.h b/packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.h new file mode 100644 index 0000000..44d2dfc --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/message_dialog_c.h @@ -0,0 +1,70 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "dialog_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque MessageDialog handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_MESSAGE_DIALOG rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_message_dialog_t; + +/// Never refers to a live MessageDialog. +#define NATIVE_INVALID_MESSAGE_DIALOG ((native_message_dialog_t)0) + +/// Creates a MessageDialog instance; release it with native_message_dialog_free(). +FFI_PLUGIN_EXPORT +native_message_dialog_t native_message_dialog_create(const char* title, const char* message); + +FFI_PLUGIN_EXPORT +void native_message_dialog_set_title(native_message_dialog_t message_dialog, const char* title); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_message_dialog_get_title(native_message_dialog_t message_dialog); + +FFI_PLUGIN_EXPORT +void native_message_dialog_set_message(native_message_dialog_t message_dialog, const char* message); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_message_dialog_get_message(native_message_dialog_t message_dialog); + +FFI_PLUGIN_EXPORT +native_dialog_modality_t native_message_dialog_get_modality(native_message_dialog_t message_dialog); + +FFI_PLUGIN_EXPORT +void native_message_dialog_set_modality(native_message_dialog_t message_dialog, native_dialog_modality_t modality); + +FFI_PLUGIN_EXPORT +bool native_message_dialog_open(native_message_dialog_t message_dialog); + +FFI_PLUGIN_EXPORT +bool native_message_dialog_close(native_message_dialog_t message_dialog); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_message_dialog_free(native_message_dialog_t message_dialog); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/placement_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/placement_c.cpp new file mode 100644 index 0000000..0b9e067 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/placement_c.cpp @@ -0,0 +1,17 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "placement_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../placement.h" + diff --git a/packages/cnativeapi/cxx_impl/src/capi/placement_c.h b/packages/cnativeapi/cxx_impl/src/capi/placement_c.h new file mode 100644 index 0000000..179312c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/placement_c.h @@ -0,0 +1,111 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + NATIVE_PLACEMENT_TOP = 0, + NATIVE_PLACEMENT_TOP_START = 1, + NATIVE_PLACEMENT_TOP_END = 2, + NATIVE_PLACEMENT_RIGHT = 3, + NATIVE_PLACEMENT_RIGHT_START = 4, + NATIVE_PLACEMENT_RIGHT_END = 5, + NATIVE_PLACEMENT_BOTTOM = 6, + NATIVE_PLACEMENT_BOTTOM_START = 7, + NATIVE_PLACEMENT_BOTTOM_END = 8, + NATIVE_PLACEMENT_LEFT = 9, + NATIVE_PLACEMENT_LEFT_START = 10, + NATIVE_PLACEMENT_LEFT_END = 11, +} native_placement_t; + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../placement.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_placement_t to_c_placement(nativeapi::Placement value); +inline nativeapi::Placement to_cpp_placement(native_placement_t value); + +inline native_placement_t to_c_placement(nativeapi::Placement value) { + switch (value) { + case nativeapi::Placement::Top: + return NATIVE_PLACEMENT_TOP; + case nativeapi::Placement::TopStart: + return NATIVE_PLACEMENT_TOP_START; + case nativeapi::Placement::TopEnd: + return NATIVE_PLACEMENT_TOP_END; + case nativeapi::Placement::Right: + return NATIVE_PLACEMENT_RIGHT; + case nativeapi::Placement::RightStart: + return NATIVE_PLACEMENT_RIGHT_START; + case nativeapi::Placement::RightEnd: + return NATIVE_PLACEMENT_RIGHT_END; + case nativeapi::Placement::Bottom: + return NATIVE_PLACEMENT_BOTTOM; + case nativeapi::Placement::BottomStart: + return NATIVE_PLACEMENT_BOTTOM_START; + case nativeapi::Placement::BottomEnd: + return NATIVE_PLACEMENT_BOTTOM_END; + case nativeapi::Placement::Left: + return NATIVE_PLACEMENT_LEFT; + case nativeapi::Placement::LeftStart: + return NATIVE_PLACEMENT_LEFT_START; + case nativeapi::Placement::LeftEnd: + return NATIVE_PLACEMENT_LEFT_END; + default: + return NATIVE_PLACEMENT_TOP; + } +} + +inline nativeapi::Placement to_cpp_placement(native_placement_t value) { + switch (value) { + case NATIVE_PLACEMENT_TOP: + return nativeapi::Placement::Top; + case NATIVE_PLACEMENT_TOP_START: + return nativeapi::Placement::TopStart; + case NATIVE_PLACEMENT_TOP_END: + return nativeapi::Placement::TopEnd; + case NATIVE_PLACEMENT_RIGHT: + return nativeapi::Placement::Right; + case NATIVE_PLACEMENT_RIGHT_START: + return nativeapi::Placement::RightStart; + case NATIVE_PLACEMENT_RIGHT_END: + return nativeapi::Placement::RightEnd; + case NATIVE_PLACEMENT_BOTTOM: + return nativeapi::Placement::Bottom; + case NATIVE_PLACEMENT_BOTTOM_START: + return nativeapi::Placement::BottomStart; + case NATIVE_PLACEMENT_BOTTOM_END: + return nativeapi::Placement::BottomEnd; + case NATIVE_PLACEMENT_LEFT: + return nativeapi::Placement::Left; + case NATIVE_PLACEMENT_LEFT_START: + return nativeapi::Placement::LeftStart; + case NATIVE_PLACEMENT_LEFT_END: + return nativeapi::Placement::LeftEnd; + default: + return nativeapi::Placement::Top; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.cpp new file mode 100644 index 0000000..5f35923 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.cpp @@ -0,0 +1,136 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "positioning_strategy_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" +#include "geometry_c.h" +#include "../window.h" +#include "window_c.h" +#include "../positioning_strategy.h" + +native_positioning_strategy_t native_positioning_strategy_absolute(native_point_t point) { + try { + auto point_cpp = to_cpp_point(point); + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(nativeapi::PositioningStrategy::Absolute(point_cpp))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_absolute"); + return 0; + } +} + +native_positioning_strategy_t native_positioning_strategy_cursor_position(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(nativeapi::PositioningStrategy::CursorPosition())); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_cursor_position"); + return 0; + } +} + +native_positioning_strategy_t native_positioning_strategy_relative_with_rect_and_offset(native_rectangle_t rect, native_point_t offset) { + try { + auto rect_cpp = to_cpp_rectangle(rect); + auto offset_cpp = to_cpp_point(offset); + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(nativeapi::PositioningStrategy::Relative(rect_cpp, offset_cpp))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_relative_with_rect_and_offset"); + return 0; + } +} + +native_positioning_strategy_t native_positioning_strategy_relative_with_window_and_offset(native_window_t window, native_point_t offset) { + try { + auto window_cpp = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!window_cpp) { + return 0; + } + auto offset_cpp = to_cpp_point(offset); + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(nativeapi::PositioningStrategy::Relative(*window_cpp, offset_cpp))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_relative_with_window_and_offset"); + return 0; + } +} + +native_positioning_strategy_type_t native_positioning_strategy_get_type(native_positioning_strategy_t positioning_strategy) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(positioning_strategy); + if (!self) { + return (native_positioning_strategy_type_t)NATIVE_POSITIONING_STRATEGY_TYPE_ABSOLUTE; + } + try { + return to_c_positioning_strategy_type(self->GetType()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_get_type"); + return (native_positioning_strategy_type_t)NATIVE_POSITIONING_STRATEGY_TYPE_ABSOLUTE; + } +} + +native_point_t native_positioning_strategy_get_absolute_position(native_positioning_strategy_t positioning_strategy) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(positioning_strategy); + if (!self) { + native_point_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetAbsolutePosition(); + return to_c_point(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_get_absolute_position"); + native_point_t result = {}; + return result; + } +} + +native_rectangle_t native_positioning_strategy_get_relative_rectangle(native_positioning_strategy_t positioning_strategy) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(positioning_strategy); + if (!self) { + native_rectangle_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetRelativeRectangle(); + return to_c_rectangle(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_get_relative_rectangle"); + native_rectangle_t result = {}; + return result; + } +} + +native_point_t native_positioning_strategy_get_relative_offset(native_positioning_strategy_t positioning_strategy) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(positioning_strategy); + if (!self) { + native_point_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetRelativeOffset(); + return to_c_point(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_positioning_strategy_get_relative_offset"); + native_point_t result = {}; + return result; + } +} + +void native_positioning_strategy_free(native_positioning_strategy_t positioning_strategy) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(positioning_strategy); +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.h b/packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.h new file mode 100644 index 0000000..e0845ff --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/positioning_strategy_c.h @@ -0,0 +1,112 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "geometry_c.h" +#include "window_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + NATIVE_POSITIONING_STRATEGY_TYPE_ABSOLUTE = 0, + NATIVE_POSITIONING_STRATEGY_TYPE_CURSOR_POSITION = 1, + NATIVE_POSITIONING_STRATEGY_TYPE_RELATIVE = 2, +} native_positioning_strategy_type_t; + +/// Opaque PositioningStrategy handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_POSITIONING_STRATEGY rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_positioning_strategy_t; + +/// Never refers to a live PositioningStrategy. +#define NATIVE_INVALID_POSITIONING_STRATEGY ((native_positioning_strategy_t)0) + +/// Caller owns the returned handle; release it with native_positioning_strategy_free(). +FFI_PLUGIN_EXPORT +native_positioning_strategy_t native_positioning_strategy_absolute(native_point_t point); + +/// Caller owns the returned handle; release it with native_positioning_strategy_free(). +FFI_PLUGIN_EXPORT +native_positioning_strategy_t native_positioning_strategy_cursor_position(void); + +/// Caller owns the returned handle; release it with native_positioning_strategy_free(). +FFI_PLUGIN_EXPORT +native_positioning_strategy_t native_positioning_strategy_relative_with_rect_and_offset(native_rectangle_t rect, native_point_t offset); + +/// Caller owns the returned handle; release it with native_positioning_strategy_free(). +FFI_PLUGIN_EXPORT +native_positioning_strategy_t native_positioning_strategy_relative_with_window_and_offset(native_window_t window, native_point_t offset); + +FFI_PLUGIN_EXPORT +native_positioning_strategy_type_t native_positioning_strategy_get_type(native_positioning_strategy_t positioning_strategy); + +FFI_PLUGIN_EXPORT +native_point_t native_positioning_strategy_get_absolute_position(native_positioning_strategy_t positioning_strategy); + +FFI_PLUGIN_EXPORT +native_rectangle_t native_positioning_strategy_get_relative_rectangle(native_positioning_strategy_t positioning_strategy); + +FFI_PLUGIN_EXPORT +native_point_t native_positioning_strategy_get_relative_offset(native_positioning_strategy_t positioning_strategy); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_positioning_strategy_free(native_positioning_strategy_t positioning_strategy); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../positioning_strategy.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_positioning_strategy_type_t to_c_positioning_strategy_type(nativeapi::PositioningStrategy::Type value); +inline nativeapi::PositioningStrategy::Type to_cpp_positioning_strategy_type(native_positioning_strategy_type_t value); + +inline native_positioning_strategy_type_t to_c_positioning_strategy_type(nativeapi::PositioningStrategy::Type value) { + switch (value) { + case nativeapi::PositioningStrategy::Type::Absolute: + return NATIVE_POSITIONING_STRATEGY_TYPE_ABSOLUTE; + case nativeapi::PositioningStrategy::Type::CursorPosition: + return NATIVE_POSITIONING_STRATEGY_TYPE_CURSOR_POSITION; + case nativeapi::PositioningStrategy::Type::Relative: + return NATIVE_POSITIONING_STRATEGY_TYPE_RELATIVE; + default: + return NATIVE_POSITIONING_STRATEGY_TYPE_ABSOLUTE; + } +} + +inline nativeapi::PositioningStrategy::Type to_cpp_positioning_strategy_type(native_positioning_strategy_type_t value) { + switch (value) { + case NATIVE_POSITIONING_STRATEGY_TYPE_ABSOLUTE: + return nativeapi::PositioningStrategy::Type::Absolute; + case NATIVE_POSITIONING_STRATEGY_TYPE_CURSOR_POSITION: + return nativeapi::PositioningStrategy::Type::CursorPosition; + case NATIVE_POSITIONING_STRATEGY_TYPE_RELATIVE: + return nativeapi::PositioningStrategy::Type::Relative; + default: + return nativeapi::PositioningStrategy::Type::Absolute; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/preferences_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/preferences_c.cpp new file mode 100644 index 0000000..407bc33 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/preferences_c.cpp @@ -0,0 +1,164 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "preferences_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../preferences.h" + +native_preferences_t native_preferences_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_create"); + return 0; + } +} + +native_preferences_t native_preferences_create_with_scope(const char* scope) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(std::string(scope ? scope : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_create_with_scope"); + return 0; + } +} + +bool native_preferences_set(native_preferences_t preferences, const char* key, const char* value) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return false; + } + try { + return self->Set(std::string(key ? key : ""), std::string(value ? value : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_set"); + return false; + } +} + +char* native_preferences_get(native_preferences_t preferences, const char* key, const char* default_value) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->Get(std::string(key ? key : ""), std::string(default_value ? default_value : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_get"); + return nullptr; + } +} + +bool native_preferences_remove(native_preferences_t preferences, const char* key) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return false; + } + try { + return self->Remove(std::string(key ? key : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_remove"); + return false; + } +} + +bool native_preferences_clear(native_preferences_t preferences) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return false; + } + try { + return self->Clear(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_clear"); + return false; + } +} + +bool native_preferences_contains(native_preferences_t preferences, const char* key) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return false; + } + try { + return self->Contains(std::string(key ? key : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_contains"); + return false; + } +} + +native_string_list_t native_preferences_get_keys(native_preferences_t preferences) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + native_string_list_t empty = {}; + return empty; + } + try { + return to_c_string_list(self->GetKeys()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_get_keys"); + native_string_list_t empty = {}; + return empty; + } +} + +unsigned long native_preferences_get_size(native_preferences_t preferences) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return 0; + } + try { + return self->GetSize(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_get_size"); + return 0; + } +} + +native_string_map_t native_preferences_get_all(native_preferences_t preferences) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + native_string_map_t empty = {}; + return empty; + } + try { + return to_c_string_map(self->GetAll()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_get_all"); + native_string_map_t empty = {}; + return empty; + } +} + +char* native_preferences_get_scope(native_preferences_t preferences) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(preferences); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetScope()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_preferences_get_scope"); + return nullptr; + } +} + +void native_preferences_free(native_preferences_t preferences) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(preferences); +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/preferences_c.h b/packages/cnativeapi/cxx_impl/src/capi/preferences_c.h new file mode 100644 index 0000000..25ddc6c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/preferences_c.h @@ -0,0 +1,77 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "string_utils_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque Preferences handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_PREFERENCES rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_preferences_t; + +/// Never refers to a live Preferences. +#define NATIVE_INVALID_PREFERENCES ((native_preferences_t)0) + +/// Creates a Preferences instance; release it with native_preferences_free(). +FFI_PLUGIN_EXPORT +native_preferences_t native_preferences_create(void); + +/// Creates a Preferences instance; release it with native_preferences_free(). +FFI_PLUGIN_EXPORT +native_preferences_t native_preferences_create_with_scope(const char* scope); + +FFI_PLUGIN_EXPORT +bool native_preferences_set(native_preferences_t preferences, const char* key, const char* value); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_preferences_get(native_preferences_t preferences, const char* key, const char* default_value); + +FFI_PLUGIN_EXPORT +bool native_preferences_remove(native_preferences_t preferences, const char* key); + +FFI_PLUGIN_EXPORT +bool native_preferences_clear(native_preferences_t preferences); + +FFI_PLUGIN_EXPORT +bool native_preferences_contains(native_preferences_t preferences, const char* key); + +FFI_PLUGIN_EXPORT +native_string_list_t native_preferences_get_keys(native_preferences_t preferences); + +FFI_PLUGIN_EXPORT +unsigned long native_preferences_get_size(native_preferences_t preferences); + +FFI_PLUGIN_EXPORT +native_string_map_t native_preferences_get_all(native_preferences_t preferences); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_preferences_get_scope(native_preferences_t preferences); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_preferences_free(native_preferences_t preferences); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.cpp new file mode 100644 index 0000000..6aaa5f5 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.cpp @@ -0,0 +1,173 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "secure_storage_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../secure_storage.h" + +native_secure_storage_t native_secure_storage_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_create"); + return 0; + } +} + +native_secure_storage_t native_secure_storage_create_with_scope(const char* scope) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(std::string(scope ? scope : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_create_with_scope"); + return 0; + } +} + +bool native_secure_storage_set(native_secure_storage_t secure_storage, const char* key, const char* value) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return false; + } + try { + return self->Set(std::string(key ? key : ""), std::string(value ? value : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_set"); + return false; + } +} + +char* native_secure_storage_get(native_secure_storage_t secure_storage, const char* key, const char* default_value) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->Get(std::string(key ? key : ""), std::string(default_value ? default_value : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_get"); + return nullptr; + } +} + +bool native_secure_storage_remove(native_secure_storage_t secure_storage, const char* key) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return false; + } + try { + return self->Remove(std::string(key ? key : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_remove"); + return false; + } +} + +bool native_secure_storage_clear(native_secure_storage_t secure_storage) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return false; + } + try { + return self->Clear(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_clear"); + return false; + } +} + +bool native_secure_storage_contains(native_secure_storage_t secure_storage, const char* key) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return false; + } + try { + return self->Contains(std::string(key ? key : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_contains"); + return false; + } +} + +native_string_list_t native_secure_storage_get_keys(native_secure_storage_t secure_storage) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + native_string_list_t empty = {}; + return empty; + } + try { + return to_c_string_list(self->GetKeys()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_get_keys"); + native_string_list_t empty = {}; + return empty; + } +} + +unsigned long native_secure_storage_get_size(native_secure_storage_t secure_storage) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return 0; + } + try { + return self->GetSize(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_get_size"); + return 0; + } +} + +native_string_map_t native_secure_storage_get_all(native_secure_storage_t secure_storage) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + native_string_map_t empty = {}; + return empty; + } + try { + return to_c_string_map(self->GetAll()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_get_all"); + native_string_map_t empty = {}; + return empty; + } +} + +char* native_secure_storage_get_scope(native_secure_storage_t secure_storage) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(secure_storage); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetScope()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_get_scope"); + return nullptr; + } +} + +bool native_secure_storage_is_available(void) { + try { + return nativeapi::SecureStorage::IsAvailable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_secure_storage_is_available"); + return false; + } +} + +void native_secure_storage_free(native_secure_storage_t secure_storage) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(secure_storage); +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.h b/packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.h new file mode 100644 index 0000000..d1a006e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/secure_storage_c.h @@ -0,0 +1,80 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "string_utils_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque SecureStorage handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_SECURE_STORAGE rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_secure_storage_t; + +/// Never refers to a live SecureStorage. +#define NATIVE_INVALID_SECURE_STORAGE ((native_secure_storage_t)0) + +/// Creates a SecureStorage instance; release it with native_secure_storage_free(). +FFI_PLUGIN_EXPORT +native_secure_storage_t native_secure_storage_create(void); + +/// Creates a SecureStorage instance; release it with native_secure_storage_free(). +FFI_PLUGIN_EXPORT +native_secure_storage_t native_secure_storage_create_with_scope(const char* scope); + +FFI_PLUGIN_EXPORT +bool native_secure_storage_set(native_secure_storage_t secure_storage, const char* key, const char* value); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_secure_storage_get(native_secure_storage_t secure_storage, const char* key, const char* default_value); + +FFI_PLUGIN_EXPORT +bool native_secure_storage_remove(native_secure_storage_t secure_storage, const char* key); + +FFI_PLUGIN_EXPORT +bool native_secure_storage_clear(native_secure_storage_t secure_storage); + +FFI_PLUGIN_EXPORT +bool native_secure_storage_contains(native_secure_storage_t secure_storage, const char* key); + +FFI_PLUGIN_EXPORT +native_string_list_t native_secure_storage_get_keys(native_secure_storage_t secure_storage); + +FFI_PLUGIN_EXPORT +unsigned long native_secure_storage_get_size(native_secure_storage_t secure_storage); + +FFI_PLUGIN_EXPORT +native_string_map_t native_secure_storage_get_all(native_secure_storage_t secure_storage); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_secure_storage_get_scope(native_secure_storage_t secure_storage); + +FFI_PLUGIN_EXPORT +bool native_secure_storage_is_available(void); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_secure_storage_free(native_secure_storage_t secure_storage); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/shortcut_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/shortcut_c.cpp new file mode 100644 index 0000000..3b53a56 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/shortcut_c.cpp @@ -0,0 +1,246 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "shortcut_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../shortcut.h" + +void native_shortcut_options_free(native_shortcut_options_t* value) { + if (!value) { + return; + } + free_c_str(value->accelerator); + value->accelerator = nullptr; + free_c_str(value->description); + value->description = nullptr; +} + +native_shortcut_t native_shortcut_create_with_id_and_options(native_shortcut_id_t id, native_shortcut_options_t options) { + try { + auto options_cpp = to_cpp_shortcut_options(options); + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(id, options_cpp)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_create_with_id_and_options"); + return 0; + } +} + +native_shortcut_t native_shortcut_create_with_id_and_accelerator_and_callback(native_shortcut_id_t id, const char* accelerator, native_shortcut_create_with_id_and_accelerator_and_callback_t callback, void* callback_user_data) { + try { + std::function callback_cpp; + if (callback) { + callback_cpp = [callback, callback_user_data]() { callback(callback_user_data); }; + } + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(id, std::string(accelerator ? accelerator : ""), callback_cpp)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_create_with_id_and_accelerator_and_callback"); + return 0; + } +} + +native_shortcut_id_t native_shortcut_get_id(native_shortcut_t shortcut) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return 0; + } + try { + return self->GetId(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_get_id"); + return 0; + } +} + +char* native_shortcut_get_accelerator(native_shortcut_t shortcut) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetAccelerator()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_get_accelerator"); + return nullptr; + } +} + +char* native_shortcut_get_description(native_shortcut_t shortcut) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetDescription()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_get_description"); + return nullptr; + } +} + +void native_shortcut_set_description(native_shortcut_t shortcut, const char* description) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return; + } + try { + self->SetDescription(std::string(description ? description : "")); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_set_description"); + return; + } +} + +native_shortcut_scope_t native_shortcut_get_scope(native_shortcut_t shortcut) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return (native_shortcut_scope_t)NATIVE_SHORTCUT_SCOPE_GLOBAL; + } + try { + return to_c_shortcut_scope(self->GetScope()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_get_scope"); + return (native_shortcut_scope_t)NATIVE_SHORTCUT_SCOPE_GLOBAL; + } +} + +void native_shortcut_set_enabled(native_shortcut_t shortcut, bool enabled) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return; + } + try { + self->SetEnabled(enabled); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_set_enabled"); + return; + } +} + +bool native_shortcut_is_enabled(native_shortcut_t shortcut) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return false; + } + try { + return self->IsEnabled(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_is_enabled"); + return false; + } +} + +void native_shortcut_invoke(native_shortcut_t shortcut) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return; + } + try { + self->Invoke(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_invoke"); + return; + } +} + +void native_shortcut_set_callback(native_shortcut_t shortcut, native_shortcut_set_callback_t callback, void* callback_user_data) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(shortcut); + if (!self) { + return; + } + try { + std::function callback_cpp; + if (callback) { + callback_cpp = [callback, callback_user_data]() { callback(callback_user_data); }; + } + self->SetCallback(callback_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_set_callback"); + return; + } +} + +void native_shortcut_free(native_shortcut_t shortcut) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(shortcut); +} + +void native_shortcut_list_free(native_shortcut_list_t* list) { + if (!list || !list->shortcuts) { + return; + } + for (long i = 0; i < list->count; ++i) { + nativeapi::HandleTable::GetInstance().Release(list->shortcuts[i]); + } + delete[] list->shortcuts; + list->shortcuts = nullptr; + list->count = 0; +} + +void native_shortcut_list_release(native_shortcut_list_t* list) { + if (!list) { + return; + } + delete[] list->shortcuts; + list->shortcuts = nullptr; + list->count = 0; +} + +bool to_c_shortcut_event(const nativeapi::ShortcutEvent& event, native_shortcut_event_t* out) { + if (!out) { + return false; + } + *out = native_shortcut_event_t{}; + out->shortcut_id = event.GetShortcutId(); + out->accelerator = to_c_str(event.GetAccelerator()); + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_SHORTCUT_EVENT_TYPE_ACTIVATED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_SHORTCUT_EVENT_TYPE_REGISTERED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_SHORTCUT_EVENT_TYPE_UNREGISTERED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_SHORTCUT_EVENT_TYPE_REGISTRATION_FAILED; + out->data.registration_failed.error_message = to_c_str(typed->GetErrorMessage()); + return true; + } + return false; +} + +void free_c_shortcut_event(native_shortcut_event_t* value) { + if (!value) { + return; + } + free_c_str(value->accelerator); + value->accelerator = nullptr; + if (value->type == NATIVE_SHORTCUT_EVENT_TYPE_REGISTRATION_FAILED) { + free_c_str(value->data.registration_failed.error_message); + value->data.registration_failed.error_message = nullptr; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/shortcut_c.h b/packages/cnativeapi/cxx_impl/src/capi/shortcut_c.h new file mode 100644 index 0000000..c3c18f4 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/shortcut_c.h @@ -0,0 +1,212 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int native_shortcut_id_t; + +typedef enum { + NATIVE_SHORTCUT_SCOPE_GLOBAL = 0, + NATIVE_SHORTCUT_SCOPE_APPLICATION = 1, +} native_shortcut_scope_t; + +typedef void (*native_shortcut_options_callback_t)(void* user_data); + +typedef void (*native_shortcut_create_with_id_and_accelerator_and_callback_t)(void* user_data); + +typedef void (*native_shortcut_set_callback_t)(void* user_data); + +typedef struct { + char* accelerator; + native_shortcut_options_callback_t callback; + void* callback_user_data; + char* description; + native_shortcut_scope_t scope; + bool enabled; +} native_shortcut_options_t; + +/// Opaque Shortcut handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_SHORTCUT rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_shortcut_t; + +/// Never refers to a live Shortcut. +#define NATIVE_INVALID_SHORTCUT ((native_shortcut_t)0) + +/// Owning list of Shortcut handles. +typedef struct { + native_shortcut_t* shortcuts; + long count; +} native_shortcut_list_t; + +/// Which concrete ShortcutEvent arrived. +typedef enum { + NATIVE_SHORTCUT_EVENT_TYPE_ACTIVATED = 0, + NATIVE_SHORTCUT_EVENT_TYPE_REGISTERED = 1, + NATIVE_SHORTCUT_EVENT_TYPE_UNREGISTERED = 2, + NATIVE_SHORTCUT_EVENT_TYPE_REGISTRATION_FAILED = 3, +} native_shortcut_event_type_t; + +/// One ShortcutEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_shortcut_event_type_t type; + native_shortcut_id_t shortcut_id; + char* accelerator; + union { + struct { + char* error_message; + } registration_failed; + } data; +} native_shortcut_event_t; + +typedef void (*native_shortcut_event_callback_t)(const native_shortcut_event_t* event, void* user_data); + +/// Frees everything the struct owns. +FFI_PLUGIN_EXPORT +void native_shortcut_options_free(native_shortcut_options_t* value); + +/// Creates a Shortcut instance; release it with native_shortcut_free(). +FFI_PLUGIN_EXPORT +native_shortcut_t native_shortcut_create_with_id_and_options(native_shortcut_id_t id, native_shortcut_options_t options); + +/// Creates a Shortcut instance; release it with native_shortcut_free(). +FFI_PLUGIN_EXPORT +native_shortcut_t native_shortcut_create_with_id_and_accelerator_and_callback(native_shortcut_id_t id, const char* accelerator, native_shortcut_create_with_id_and_accelerator_and_callback_t callback, void* callback_user_data); + +FFI_PLUGIN_EXPORT +native_shortcut_id_t native_shortcut_get_id(native_shortcut_t shortcut); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_shortcut_get_accelerator(native_shortcut_t shortcut); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_shortcut_get_description(native_shortcut_t shortcut); + +FFI_PLUGIN_EXPORT +void native_shortcut_set_description(native_shortcut_t shortcut, const char* description); + +FFI_PLUGIN_EXPORT +native_shortcut_scope_t native_shortcut_get_scope(native_shortcut_t shortcut); + +FFI_PLUGIN_EXPORT +void native_shortcut_set_enabled(native_shortcut_t shortcut, bool enabled); + +FFI_PLUGIN_EXPORT +bool native_shortcut_is_enabled(native_shortcut_t shortcut); + +FFI_PLUGIN_EXPORT +void native_shortcut_invoke(native_shortcut_t shortcut); + +FFI_PLUGIN_EXPORT +void native_shortcut_set_callback(native_shortcut_t shortcut, native_shortcut_set_callback_t callback, void* callback_user_data); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_shortcut_free(native_shortcut_t shortcut); + +/// Frees the array and releases every handle it contains. +FFI_PLUGIN_EXPORT +void native_shortcut_list_free(native_shortcut_list_t* list); + +/// Frees only the array; the caller takes over the handles. +FFI_PLUGIN_EXPORT +void native_shortcut_list_release(native_shortcut_list_t* list); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class ShortcutEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_shortcut_event(const nativeapi::ShortcutEvent& event, native_shortcut_event_t* out); +/// Releases everything to_c_shortcut_event() allocated. +void free_c_shortcut_event(native_shortcut_event_t* value); + +#endif + +#ifdef __cplusplus +#include "../shortcut.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_shortcut_scope_t to_c_shortcut_scope(nativeapi::ShortcutScope value); +inline nativeapi::ShortcutScope to_cpp_shortcut_scope(native_shortcut_scope_t value); +inline native_shortcut_options_t to_c_shortcut_options(const nativeapi::ShortcutOptions& value); +inline nativeapi::ShortcutOptions to_cpp_shortcut_options(const native_shortcut_options_t& value); + +inline native_shortcut_scope_t to_c_shortcut_scope(nativeapi::ShortcutScope value) { + switch (value) { + case nativeapi::ShortcutScope::Global: + return NATIVE_SHORTCUT_SCOPE_GLOBAL; + case nativeapi::ShortcutScope::Application: + return NATIVE_SHORTCUT_SCOPE_APPLICATION; + default: + return NATIVE_SHORTCUT_SCOPE_GLOBAL; + } +} + +inline nativeapi::ShortcutScope to_cpp_shortcut_scope(native_shortcut_scope_t value) { + switch (value) { + case NATIVE_SHORTCUT_SCOPE_GLOBAL: + return nativeapi::ShortcutScope::Global; + case NATIVE_SHORTCUT_SCOPE_APPLICATION: + return nativeapi::ShortcutScope::Application; + default: + return nativeapi::ShortcutScope::Global; + } +} + +inline native_shortcut_options_t to_c_shortcut_options(const nativeapi::ShortcutOptions& value) { + native_shortcut_options_t result = {}; + result.accelerator = to_c_str(value.accelerator); + result.description = to_c_str(value.description); + result.scope = to_c_shortcut_scope(value.scope); + result.enabled = value.enabled; + return result; +} + +inline nativeapi::ShortcutOptions to_cpp_shortcut_options(const native_shortcut_options_t& value) { + nativeapi::ShortcutOptions result = {}; + result.accelerator = value.accelerator ? value.accelerator : ""; + if (value.callback) { + auto callback = value.callback; + auto* data = value.callback_user_data; + result.callback = [callback, data]() { callback(data); }; + } + result.description = value.description ? value.description : ""; + result.scope = to_cpp_shortcut_scope(value.scope); + result.enabled = value.enabled; + return result; +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.cpp new file mode 100644 index 0000000..a8ae690 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.cpp @@ -0,0 +1,216 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "shortcut_manager_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../shortcut.h" +#include "shortcut_c.h" +#include "../shortcut_manager.h" + +bool native_shortcut_manager_is_supported(void) { + try { + return nativeapi::ShortcutManager::GetInstance().IsSupported(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_is_supported"); + return false; + } +} + +native_shortcut_t native_shortcut_manager_register_with_accelerator_and_callback(const char* accelerator, native_shortcut_manager_register_callback_t callback, void* callback_user_data) { + try { + std::function callback_cpp; + if (callback) { + callback_cpp = [callback, callback_user_data]() { callback(callback_user_data); }; + } + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::ShortcutManager::GetInstance().Register(std::string(accelerator ? accelerator : ""), callback_cpp)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_register_with_accelerator_and_callback"); + return 0; + } +} + +native_shortcut_t native_shortcut_manager_register_with_options(native_shortcut_options_t options) { + try { + auto options_cpp = to_cpp_shortcut_options(options); + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::ShortcutManager::GetInstance().Register(options_cpp)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_register_with_options"); + return 0; + } +} + +bool native_shortcut_manager_unregister_with_id(native_shortcut_id_t id) { + try { + return nativeapi::ShortcutManager::GetInstance().Unregister(id); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_unregister_with_id"); + return false; + } +} + +bool native_shortcut_manager_unregister_with_accelerator(const char* accelerator) { + try { + return nativeapi::ShortcutManager::GetInstance().Unregister(std::string(accelerator ? accelerator : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_unregister_with_accelerator"); + return false; + } +} + +int native_shortcut_manager_unregister_all(void) { + try { + return nativeapi::ShortcutManager::GetInstance().UnregisterAll(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_unregister_all"); + return 0; + } +} + +native_shortcut_t native_shortcut_manager_get_with_id(native_shortcut_id_t id) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::ShortcutManager::GetInstance().Get(id)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_get_with_id"); + return 0; + } +} + +native_shortcut_t native_shortcut_manager_get_with_accelerator(const char* accelerator) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::ShortcutManager::GetInstance().Get(std::string(accelerator ? accelerator : ""))); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_get_with_accelerator"); + return 0; + } +} + +native_shortcut_list_t native_shortcut_manager_get_all(void) { + try { + const auto items = nativeapi::ShortcutManager::GetInstance().GetAll(); + native_shortcut_list_t list = {}; + if (items.empty()) { + return list; + } + list.shortcuts = new (std::nothrow) native_shortcut_t[items.size()]; + if (!list.shortcuts) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.shortcuts[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_get_all"); + native_shortcut_list_t empty = {}; + return empty; + } +} + +native_shortcut_list_t native_shortcut_manager_get_by_scope(native_shortcut_scope_t scope) { + try { + const auto items = nativeapi::ShortcutManager::GetInstance().GetByScope(to_cpp_shortcut_scope(scope)); + native_shortcut_list_t list = {}; + if (items.empty()) { + return list; + } + list.shortcuts = new (std::nothrow) native_shortcut_t[items.size()]; + if (!list.shortcuts) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.shortcuts[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_get_by_scope"); + native_shortcut_list_t empty = {}; + return empty; + } +} + +bool native_shortcut_manager_is_available(const char* accelerator) { + try { + return nativeapi::ShortcutManager::GetInstance().IsAvailable(std::string(accelerator ? accelerator : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_is_available"); + return false; + } +} + +bool native_shortcut_manager_is_valid_accelerator(const char* accelerator) { + try { + return nativeapi::ShortcutManager::GetInstance().IsValidAccelerator(std::string(accelerator ? accelerator : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_is_valid_accelerator"); + return false; + } +} + +void native_shortcut_manager_set_enabled(bool enabled) { + try { + nativeapi::ShortcutManager::GetInstance().SetEnabled(enabled); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_set_enabled"); + return; + } +} + +bool native_shortcut_manager_is_enabled(void) { + try { + return nativeapi::ShortcutManager::GetInstance().IsEnabled(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_is_enabled"); + return false; + } +} + +void native_shortcut_manager_emit_shortcut_activated(native_shortcut_id_t id, const char* accelerator) { + try { + nativeapi::ShortcutManager::GetInstance().EmitShortcutActivated(id, std::string(accelerator ? accelerator : "")); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_shortcut_manager_emit_shortcut_activated"); + return; + } +} + +native_listener_id_t native_shortcut_manager_add_listener(native_shortcut_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + try { + return static_cast(nativeapi::ShortcutManager::GetInstance().AddListener( + [callback, user_data](const nativeapi::ShortcutEvent& event) { + native_shortcut_event_t c_event = {}; + if (!to_c_shortcut_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_shortcut_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_shortcut_manager_remove_listener(native_listener_id_t listener_id) { + try { + return nativeapi::ShortcutManager::GetInstance().RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.h b/packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.h new file mode 100644 index 0000000..098a653 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/shortcut_manager_c.h @@ -0,0 +1,84 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "shortcut_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*native_shortcut_manager_register_callback_t)(void* user_data); + +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_is_supported(void); + +/// Caller owns the returned handle; release it with native_shortcut_free(). +FFI_PLUGIN_EXPORT +native_shortcut_t native_shortcut_manager_register_with_accelerator_and_callback(const char* accelerator, native_shortcut_manager_register_callback_t callback, void* callback_user_data); + +/// Caller owns the returned handle; release it with native_shortcut_free(). +FFI_PLUGIN_EXPORT +native_shortcut_t native_shortcut_manager_register_with_options(native_shortcut_options_t options); + +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_unregister_with_id(native_shortcut_id_t id); + +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_unregister_with_accelerator(const char* accelerator); + +FFI_PLUGIN_EXPORT +int native_shortcut_manager_unregister_all(void); + +/// Caller owns the returned handle; release it with native_shortcut_free(). +FFI_PLUGIN_EXPORT +native_shortcut_t native_shortcut_manager_get_with_id(native_shortcut_id_t id); + +/// Caller owns the returned handle; release it with native_shortcut_free(). +FFI_PLUGIN_EXPORT +native_shortcut_t native_shortcut_manager_get_with_accelerator(const char* accelerator); + +FFI_PLUGIN_EXPORT +native_shortcut_list_t native_shortcut_manager_get_all(void); + +FFI_PLUGIN_EXPORT +native_shortcut_list_t native_shortcut_manager_get_by_scope(native_shortcut_scope_t scope); + +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_is_available(const char* accelerator); + +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_is_valid_accelerator(const char* accelerator); + +FFI_PLUGIN_EXPORT +void native_shortcut_manager_set_enabled(bool enabled); + +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_is_enabled(void); + +FFI_PLUGIN_EXPORT +void native_shortcut_manager_emit_shortcut_activated(native_shortcut_id_t id, const char* accelerator); + +/// Registers @p callback for every ShortcutEvent this ShortcutManager emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_shortcut_manager_add_listener(native_shortcut_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_shortcut_manager_remove_listener(native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/string_utils_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/string_utils_c.cpp new file mode 100644 index 0000000..b1c058e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/string_utils_c.cpp @@ -0,0 +1,90 @@ +#include "string_utils_c.h" +#include + +char* to_c_str(const std::string& str) { + if (str.empty()) + return nullptr; + + size_t len = str.length() + 1; + char* result = new (std::nothrow) char[len]; + if (result) { + std::strcpy(result, str.c_str()); + } + return result; +} + +native_string_list_t to_c_string_list(const std::vector& values) { + native_string_list_t list = {}; + if (values.empty()) + return list; + + list.items = new (std::nothrow) char*[values.size()](); + if (!list.items) + return list; + + for (size_t i = 0; i < values.size(); i++) { + list.items[i] = to_c_str(values[i]); + } + list.count = static_cast(values.size()); + return list; +} + +native_string_map_t to_c_string_map(const std::map& values) { + native_string_map_t map = {}; + if (values.empty()) + return map; + + map.keys = new (std::nothrow) char*[values.size()](); + map.values = new (std::nothrow) char*[values.size()](); + if (!map.keys || !map.values) { + delete[] map.keys; + delete[] map.values; + map.keys = nullptr; + map.values = nullptr; + return map; + } + + size_t index = 0; + for (const auto& entry : values) { + map.keys[index] = to_c_str(entry.first); + map.values[index] = to_c_str(entry.second); + index++; + } + map.count = static_cast(values.size()); + return map; +} + +void free_c_str(char* str) { + if (str) { + delete[] str; + } +} + +void native_string_list_free(native_string_list_t* list) { + if (!list || !list->items) + return; + + for (long i = 0; i < list->count; i++) { + free_c_str(list->items[i]); + } + delete[] list->items; + list->items = nullptr; + list->count = 0; +} + +void native_string_map_free(native_string_map_t* map) { + if (!map) + return; + + for (long i = 0; i < map->count; i++) { + if (map->keys) + free_c_str(map->keys[i]); + if (map->values) + free_c_str(map->values[i]); + } + delete[] map->keys; + delete[] map->values; + map->keys = nullptr; + map->values = nullptr; + map->count = 0; +} diff --git a/packages/cnativeapi/cxx_impl/src/capi/string_utils_c.h b/packages/cnativeapi/cxx_impl/src/capi/string_utils_c.h new file mode 100644 index 0000000..19410ba --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/string_utils_c.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#include +#include +#endif + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * String utilities for C API interoperability + */ + +/** + * An owning list of strings. + * + * Free with native_string_list_free(); it releases every item and the array. + */ +typedef struct { + char** items; + long count; +} native_string_list_t; + +/** + * An owning list of string key/value pairs. `keys[i]` corresponds to + * `values[i]`. + * + * Free with native_string_map_free(); it releases every entry and the arrays. + */ +typedef struct { + char** keys; + char** values; + long count; +} native_string_map_t; + +/** + * Convert a C++ string to a C string with memory allocation + * @param str The C++ string to convert + * @return Allocated C string copy, or nullptr if str is empty or allocation + * failed. Caller must free the returned string with free_c_str(). + */ +#ifdef __cplusplus +char* to_c_str(const std::string& str); + +/** + * Convert a C++ string vector to an owning C string list. + * @param values The strings to copy + * @return List owned by the caller; free it with native_string_list_free(). + */ +native_string_list_t to_c_string_list(const std::vector& values); + +/** + * Convert a C++ string map to an owning C string map. + * @param values The entries to copy + * @return Map owned by the caller; free it with native_string_map_free(). + */ +native_string_map_t to_c_string_map(const std::map& values); +#endif + +/** + * Free a C string allocated by to_c_str + * @param str The string to free (can be nullptr) + */ +FFI_PLUGIN_EXPORT +void free_c_str(char* str); + +/** + * Free a string list allocated by to_c_string_list + * @param list The list to free (can be nullptr) + */ +FFI_PLUGIN_EXPORT +void native_string_list_free(native_string_list_t* list); + +/** + * Free a string map allocated by to_c_string_map + * @param map The map to free (can be nullptr) + */ +FFI_PLUGIN_EXPORT +void native_string_map_free(native_string_map_t* map); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.cpp new file mode 100644 index 0000000..f1d0df2 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.cpp @@ -0,0 +1,370 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "tray_icon_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" +#include "geometry_c.h" +#include "../image.h" +#include "image_c.h" +#include "../menu.h" +#include "menu_c.h" +#include "../tray_icon.h" + +native_tray_icon_t native_tray_icon_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_create"); + return 0; + } +} + +native_tray_icon_t native_tray_icon_create_with_tray(void* tray) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(tray)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_create_with_tray"); + return 0; + } +} + +native_tray_icon_id_t native_tray_icon_get_id(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return 0; + } + try { + return self->GetId(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_id"); + return 0; + } +} + +void native_tray_icon_set_icon(native_tray_icon_t tray_icon, native_image_t image) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return; + } + try { + auto image_cpp = nativeapi::HandleTable::GetInstance().Resolve(image); + self->SetIcon(image_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_set_icon"); + return; + } +} + +native_image_t native_tray_icon_get_icon(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return 0; + } + try { + return nativeapi::HandleTable::GetInstance().Insert(self->GetIcon()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_icon"); + return 0; + } +} + +void native_tray_icon_set_title(native_tray_icon_t tray_icon, const char* title) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return; + } + try { + std::optional title_cpp; + if (title) { + title_cpp = std::string(title); + } + self->SetTitle(title_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_set_title"); + return; + } +} + +char* native_tray_icon_get_title(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return nullptr; + } + try { + const auto cpp_result = self->GetTitle(); + return cpp_result ? to_c_str(*cpp_result) : nullptr; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_title"); + return nullptr; + } +} + +void native_tray_icon_set_tooltip(native_tray_icon_t tray_icon, const char* tooltip) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return; + } + try { + std::optional tooltip_cpp; + if (tooltip) { + tooltip_cpp = std::string(tooltip); + } + self->SetTooltip(tooltip_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_set_tooltip"); + return; + } +} + +char* native_tray_icon_get_tooltip(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return nullptr; + } + try { + const auto cpp_result = self->GetTooltip(); + return cpp_result ? to_c_str(*cpp_result) : nullptr; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_tooltip"); + return nullptr; + } +} + +void native_tray_icon_set_context_menu(native_tray_icon_t tray_icon, native_menu_t menu) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return; + } + try { + auto menu_cpp = nativeapi::HandleTable::GetInstance().Resolve(menu); + self->SetContextMenu(menu_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_set_context_menu"); + return; + } +} + +native_menu_t native_tray_icon_get_context_menu(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return 0; + } + try { + return nativeapi::HandleTable::GetInstance().Insert(self->GetContextMenu()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_context_menu"); + return 0; + } +} + +void native_tray_icon_set_context_menu_trigger(native_tray_icon_t tray_icon, native_context_menu_trigger_t trigger) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return; + } + try { + self->SetContextMenuTrigger(to_cpp_context_menu_trigger(trigger)); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_set_context_menu_trigger"); + return; + } +} + +native_context_menu_trigger_t native_tray_icon_get_context_menu_trigger(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return (native_context_menu_trigger_t)NATIVE_CONTEXT_MENU_TRIGGER_NONE; + } + try { + return to_c_context_menu_trigger(self->GetContextMenuTrigger()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_context_menu_trigger"); + return (native_context_menu_trigger_t)NATIVE_CONTEXT_MENU_TRIGGER_NONE; + } +} + +native_rectangle_t native_tray_icon_get_bounds(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + native_rectangle_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetBounds(); + return to_c_rectangle(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_get_bounds"); + native_rectangle_t result = {}; + return result; + } +} + +bool native_tray_icon_set_visible(native_tray_icon_t tray_icon, bool visible) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return false; + } + try { + return self->SetVisible(visible); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_set_visible"); + return false; + } +} + +bool native_tray_icon_is_visible(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return false; + } + try { + return self->IsVisible(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_is_visible"); + return false; + } +} + +bool native_tray_icon_open_context_menu(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return false; + } + try { + return self->OpenContextMenu(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_open_context_menu"); + return false; + } +} + +bool native_tray_icon_close_context_menu(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return false; + } + try { + return self->CloseContextMenu(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_icon_close_context_menu"); + return false; + } +} + +void* native_tray_icon_get_native_object(native_tray_icon_t tray_icon) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return nullptr; + } + return self->GetNativeObject(); +} + +void native_tray_icon_free(native_tray_icon_t tray_icon) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(tray_icon); +} + +void native_tray_icon_list_free(native_tray_icon_list_t* list) { + if (!list || !list->tray_icons) { + return; + } + for (long i = 0; i < list->count; ++i) { + nativeapi::HandleTable::GetInstance().Release(list->tray_icons[i]); + } + delete[] list->tray_icons; + list->tray_icons = nullptr; + list->count = 0; +} + +void native_tray_icon_list_release(native_tray_icon_list_t* list) { + if (!list) { + return; + } + delete[] list->tray_icons; + list->tray_icons = nullptr; + list->count = 0; +} + +native_listener_id_t native_tray_icon_add_listener(native_tray_icon_t tray_icon, native_tray_icon_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return 0; + } + try { + return static_cast(self->AddListener( + [callback, user_data](const nativeapi::TrayIconEvent& event) { + native_tray_icon_event_t c_event = {}; + if (!to_c_tray_icon_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_tray_icon_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_tray_icon_remove_listener(native_tray_icon_t tray_icon, native_listener_id_t listener_id) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(tray_icon); + if (!self) { + return false; + } + try { + return self->RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + +bool to_c_tray_icon_event(const nativeapi::TrayIconEvent& event, native_tray_icon_event_t* out) { + if (!out) { + return false; + } + *out = native_tray_icon_event_t{}; + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_TRAY_ICON_EVENT_TYPE_CLICKED; + out->data.clicked.tray_icon_id = typed->GetTrayIconId(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_TRAY_ICON_EVENT_TYPE_RIGHT_CLICKED; + out->data.right_clicked.tray_icon_id = typed->GetTrayIconId(); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_TRAY_ICON_EVENT_TYPE_DOUBLE_CLICKED; + out->data.double_clicked.tray_icon_id = typed->GetTrayIconId(); + return true; + } + return false; +} + +void free_c_tray_icon_event(native_tray_icon_event_t* value) { + if (!value) { + return; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.h b/packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.h new file mode 100644 index 0000000..3336b6a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/tray_icon_c.h @@ -0,0 +1,220 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "geometry_c.h" +#include "image_c.h" +#include "menu_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int native_tray_icon_id_t; + +typedef enum { + NATIVE_CONTEXT_MENU_TRIGGER_NONE = 0, + NATIVE_CONTEXT_MENU_TRIGGER_CLICKED = 1, + NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED = 2, + NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED = 3, +} native_context_menu_trigger_t; + +/// Opaque TrayIcon handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_TRAY_ICON rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_tray_icon_t; + +/// Never refers to a live TrayIcon. +#define NATIVE_INVALID_TRAY_ICON ((native_tray_icon_t)0) + +/// Owning list of TrayIcon handles. +typedef struct { + native_tray_icon_t* tray_icons; + long count; +} native_tray_icon_list_t; + +/// Which concrete TrayIconEvent arrived. +typedef enum { + NATIVE_TRAY_ICON_EVENT_TYPE_CLICKED = 0, + NATIVE_TRAY_ICON_EVENT_TYPE_RIGHT_CLICKED = 1, + NATIVE_TRAY_ICON_EVENT_TYPE_DOUBLE_CLICKED = 2, +} native_tray_icon_event_type_t; + +/// One TrayIconEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_tray_icon_event_type_t type; + union { + struct { + native_tray_icon_id_t tray_icon_id; + } clicked; + struct { + native_tray_icon_id_t tray_icon_id; + } right_clicked; + struct { + native_tray_icon_id_t tray_icon_id; + } double_clicked; + } data; +} native_tray_icon_event_t; + +typedef void (*native_tray_icon_event_callback_t)(const native_tray_icon_event_t* event, void* user_data); + +/// Creates a TrayIcon instance; release it with native_tray_icon_free(). +FFI_PLUGIN_EXPORT +native_tray_icon_t native_tray_icon_create(void); + +/// Creates a TrayIcon instance; release it with native_tray_icon_free(). +FFI_PLUGIN_EXPORT +native_tray_icon_t native_tray_icon_create_with_tray(void* tray); + +FFI_PLUGIN_EXPORT +native_tray_icon_id_t native_tray_icon_get_id(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +void native_tray_icon_set_icon(native_tray_icon_t tray_icon, native_image_t image); + +/// Caller owns the returned handle; release it with native_image_free(). +FFI_PLUGIN_EXPORT +native_image_t native_tray_icon_get_icon(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +void native_tray_icon_set_title(native_tray_icon_t tray_icon, const char* title); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_tray_icon_get_title(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +void native_tray_icon_set_tooltip(native_tray_icon_t tray_icon, const char* tooltip); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_tray_icon_get_tooltip(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +void native_tray_icon_set_context_menu(native_tray_icon_t tray_icon, native_menu_t menu); + +/// Caller owns the returned handle; release it with native_menu_free(). +FFI_PLUGIN_EXPORT +native_menu_t native_tray_icon_get_context_menu(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +void native_tray_icon_set_context_menu_trigger(native_tray_icon_t tray_icon, native_context_menu_trigger_t trigger); + +FFI_PLUGIN_EXPORT +native_context_menu_trigger_t native_tray_icon_get_context_menu_trigger(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +native_rectangle_t native_tray_icon_get_bounds(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +bool native_tray_icon_set_visible(native_tray_icon_t tray_icon, bool visible); + +FFI_PLUGIN_EXPORT +bool native_tray_icon_is_visible(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +bool native_tray_icon_open_context_menu(native_tray_icon_t tray_icon); + +FFI_PLUGIN_EXPORT +bool native_tray_icon_close_context_menu(native_tray_icon_t tray_icon); + +/// Platform-specific native object (NSScreen*, HMONITOR, ...). +FFI_PLUGIN_EXPORT +void* native_tray_icon_get_native_object(native_tray_icon_t tray_icon); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_tray_icon_free(native_tray_icon_t tray_icon); + +/// Frees the array and releases every handle it contains. +FFI_PLUGIN_EXPORT +void native_tray_icon_list_free(native_tray_icon_list_t* list); + +/// Frees only the array; the caller takes over the handles. +FFI_PLUGIN_EXPORT +void native_tray_icon_list_release(native_tray_icon_list_t* list); + +/// Registers @p callback for every TrayIconEvent this TrayIcon emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_tray_icon_add_listener(native_tray_icon_t tray_icon, native_tray_icon_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_tray_icon_remove_listener(native_tray_icon_t tray_icon, native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class TrayIconEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_tray_icon_event(const nativeapi::TrayIconEvent& event, native_tray_icon_event_t* out); +/// Releases everything to_c_tray_icon_event() allocated. +void free_c_tray_icon_event(native_tray_icon_event_t* value); + +#endif + +#ifdef __cplusplus +#include "../tray_icon.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_context_menu_trigger_t to_c_context_menu_trigger(nativeapi::ContextMenuTrigger value); +inline nativeapi::ContextMenuTrigger to_cpp_context_menu_trigger(native_context_menu_trigger_t value); + +inline native_context_menu_trigger_t to_c_context_menu_trigger(nativeapi::ContextMenuTrigger value) { + switch (value) { + case nativeapi::ContextMenuTrigger::None: + return NATIVE_CONTEXT_MENU_TRIGGER_NONE; + case nativeapi::ContextMenuTrigger::Clicked: + return NATIVE_CONTEXT_MENU_TRIGGER_CLICKED; + case nativeapi::ContextMenuTrigger::RightClicked: + return NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED; + case nativeapi::ContextMenuTrigger::DoubleClicked: + return NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED; + default: + return NATIVE_CONTEXT_MENU_TRIGGER_NONE; + } +} + +inline nativeapi::ContextMenuTrigger to_cpp_context_menu_trigger(native_context_menu_trigger_t value) { + switch (value) { + case NATIVE_CONTEXT_MENU_TRIGGER_NONE: + return nativeapi::ContextMenuTrigger::None; + case NATIVE_CONTEXT_MENU_TRIGGER_CLICKED: + return nativeapi::ContextMenuTrigger::Clicked; + case NATIVE_CONTEXT_MENU_TRIGGER_RIGHT_CLICKED: + return nativeapi::ContextMenuTrigger::RightClicked; + case NATIVE_CONTEXT_MENU_TRIGGER_DOUBLE_CLICKED: + return nativeapi::ContextMenuTrigger::DoubleClicked; + default: + return nativeapi::ContextMenuTrigger::None; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.cpp new file mode 100644 index 0000000..ded6fff --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.cpp @@ -0,0 +1,60 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "tray_manager_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../tray_icon.h" +#include "tray_icon_c.h" +#include "../tray_manager.h" + +bool native_tray_manager_is_supported(void) { + try { + return nativeapi::TrayManager::GetInstance().IsSupported(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_manager_is_supported"); + return false; + } +} + +native_tray_icon_t native_tray_manager_get(native_tray_icon_id_t id) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::TrayManager::GetInstance().Get(id)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_manager_get"); + return 0; + } +} + +native_tray_icon_list_t native_tray_manager_get_all(void) { + try { + const auto items = nativeapi::TrayManager::GetInstance().GetAll(); + native_tray_icon_list_t list = {}; + if (items.empty()) { + return list; + } + list.tray_icons = new (std::nothrow) native_tray_icon_t[items.size()]; + if (!list.tray_icons) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.tray_icons[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_tray_manager_get_all"); + native_tray_icon_list_t empty = {}; + return empty; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.h b/packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.h new file mode 100644 index 0000000..8231d47 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/tray_manager_c.h @@ -0,0 +1,34 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "tray_icon_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +FFI_PLUGIN_EXPORT +bool native_tray_manager_is_supported(void); + +/// Caller owns the returned handle; release it with native_tray_icon_free(). +FFI_PLUGIN_EXPORT +native_tray_icon_t native_tray_manager_get(native_tray_icon_id_t id); + +FFI_PLUGIN_EXPORT +native_tray_icon_list_t native_tray_manager_get_all(void); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/capi/url_opener_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/url_opener_c.cpp new file mode 100644 index 0000000..46f1125 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/url_opener_c.cpp @@ -0,0 +1,55 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "url_opener_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../url_opener.h" + +void native_url_open_result_free(native_url_open_result_t* value) { + if (!value) { + return; + } + free_c_str(value->error_message); + value->error_message = nullptr; +} + +bool native_url_opener_is_supported(void) { + try { + return nativeapi::UrlOpener::GetInstance().IsSupported(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_url_opener_is_supported"); + return false; + } +} + +bool native_url_opener_can_open(const char* url) { + try { + return nativeapi::UrlOpener::GetInstance().CanOpen(std::string(url ? url : "")); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_url_opener_can_open"); + return false; + } +} + +native_url_open_result_t native_url_opener_open(const char* url) { + try { + const auto cpp_result = nativeapi::UrlOpener::GetInstance().Open(std::string(url ? url : "")); + return to_c_url_open_result(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_url_opener_open"); + native_url_open_result_t result = {}; + result.success = false; + return result; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/url_opener_c.h b/packages/cnativeapi/cxx_impl/src/capi/url_opener_c.h new file mode 100644 index 0000000..cc4a9fa --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/url_opener_c.h @@ -0,0 +1,118 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + NATIVE_URL_OPEN_ERROR_CODE_NONE = 0, + NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_EMPTY = 1, + NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_MISSING_SCHEME = 2, + NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_UNSUPPORTED_SCHEME = 3, + NATIVE_URL_OPEN_ERROR_CODE_UNSUPPORTED_PLATFORM = 4, + NATIVE_URL_OPEN_ERROR_CODE_INVOCATION_FAILED = 5, +} native_url_open_error_code_t; + +typedef struct { + bool success; + native_url_open_error_code_t error_code; + char* error_message; +} native_url_open_result_t; + +/// Frees everything the struct owns. +FFI_PLUGIN_EXPORT +void native_url_open_result_free(native_url_open_result_t* value); + +FFI_PLUGIN_EXPORT +bool native_url_opener_is_supported(void); + +FFI_PLUGIN_EXPORT +bool native_url_opener_can_open(const char* url); + +FFI_PLUGIN_EXPORT +native_url_open_result_t native_url_opener_open(const char* url); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include "../url_opener.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_url_open_error_code_t to_c_url_open_error_code(nativeapi::UrlOpenErrorCode value); +inline nativeapi::UrlOpenErrorCode to_cpp_url_open_error_code(native_url_open_error_code_t value); +inline native_url_open_result_t to_c_url_open_result(const nativeapi::UrlOpenResult& value); +inline nativeapi::UrlOpenResult to_cpp_url_open_result(const native_url_open_result_t& value); + +inline native_url_open_error_code_t to_c_url_open_error_code(nativeapi::UrlOpenErrorCode value) { + switch (value) { + case nativeapi::UrlOpenErrorCode::kNone: + return NATIVE_URL_OPEN_ERROR_CODE_NONE; + case nativeapi::UrlOpenErrorCode::kInvalidUrlEmpty: + return NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_EMPTY; + case nativeapi::UrlOpenErrorCode::kInvalidUrlMissingScheme: + return NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_MISSING_SCHEME; + case nativeapi::UrlOpenErrorCode::kInvalidUrlUnsupportedScheme: + return NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_UNSUPPORTED_SCHEME; + case nativeapi::UrlOpenErrorCode::kUnsupportedPlatform: + return NATIVE_URL_OPEN_ERROR_CODE_UNSUPPORTED_PLATFORM; + case nativeapi::UrlOpenErrorCode::kInvocationFailed: + return NATIVE_URL_OPEN_ERROR_CODE_INVOCATION_FAILED; + default: + return NATIVE_URL_OPEN_ERROR_CODE_NONE; + } +} + +inline nativeapi::UrlOpenErrorCode to_cpp_url_open_error_code(native_url_open_error_code_t value) { + switch (value) { + case NATIVE_URL_OPEN_ERROR_CODE_NONE: + return nativeapi::UrlOpenErrorCode::kNone; + case NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_EMPTY: + return nativeapi::UrlOpenErrorCode::kInvalidUrlEmpty; + case NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_MISSING_SCHEME: + return nativeapi::UrlOpenErrorCode::kInvalidUrlMissingScheme; + case NATIVE_URL_OPEN_ERROR_CODE_INVALID_URL_UNSUPPORTED_SCHEME: + return nativeapi::UrlOpenErrorCode::kInvalidUrlUnsupportedScheme; + case NATIVE_URL_OPEN_ERROR_CODE_UNSUPPORTED_PLATFORM: + return nativeapi::UrlOpenErrorCode::kUnsupportedPlatform; + case NATIVE_URL_OPEN_ERROR_CODE_INVOCATION_FAILED: + return nativeapi::UrlOpenErrorCode::kInvocationFailed; + default: + return nativeapi::UrlOpenErrorCode::kNone; + } +} + +inline native_url_open_result_t to_c_url_open_result(const nativeapi::UrlOpenResult& value) { + native_url_open_result_t result = {}; + result.success = value.success; + result.error_code = to_c_url_open_error_code(value.error_code); + result.error_message = to_c_str(value.error_message); + return result; +} + +inline nativeapi::UrlOpenResult to_cpp_url_open_result(const native_url_open_result_t& value) { + nativeapi::UrlOpenResult result = {}; + result.success = value.success; + result.error_code = to_cpp_url_open_error_code(value.error_code); + result.error_message = value.error_message ? value.error_message : ""; + return result; +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/window_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/window_c.cpp new file mode 100644 index 0000000..0b73ee4 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/window_c.cpp @@ -0,0 +1,1066 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "window_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../foundation/geometry.h" +#include "geometry_c.h" +#include "../foundation/color.h" +#include "color_c.h" +#include "../window.h" + +native_window_t native_window_create(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_create"); + return 0; + } +} + +native_window_t native_window_create_with_native_window(void* native_window) { + try { + return nativeapi::HandleTable::GetInstance().Insert( + std::make_shared(native_window)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_create_with_native_window"); + return 0; + } +} + +native_window_id_t native_window_get_id(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return 0; + } + try { + return self->GetId(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_id"); + return 0; + } +} + +void native_window_focus(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Focus(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_focus"); + return; + } +} + +void native_window_blur(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Blur(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_blur"); + return; + } +} + +bool native_window_is_focused(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsFocused(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_focused"); + return false; + } +} + +void native_window_show(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Show(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_show"); + return; + } +} + +void native_window_show_inactive(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->ShowInactive(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_show_inactive"); + return; + } +} + +void native_window_hide(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Hide(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_hide"); + return; + } +} + +bool native_window_is_visible(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsVisible(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_visible"); + return false; + } +} + +void native_window_maximize(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Maximize(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_maximize"); + return; + } +} + +void native_window_unmaximize(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Unmaximize(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_unmaximize"); + return; + } +} + +bool native_window_is_maximized(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsMaximized(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_maximized"); + return false; + } +} + +void native_window_minimize(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Minimize(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_minimize"); + return; + } +} + +void native_window_restore(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Restore(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_restore"); + return; + } +} + +bool native_window_is_minimized(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsMinimized(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_minimized"); + return false; + } +} + +void native_window_set_full_screen(native_window_t window, bool is_full_screen) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetFullScreen(is_full_screen); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_full_screen"); + return; + } +} + +bool native_window_is_full_screen(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsFullScreen(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_full_screen"); + return false; + } +} + +void native_window_set_bounds(native_window_t window, native_rectangle_t bounds) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto bounds_cpp = to_cpp_rectangle(bounds); + self->SetBounds(bounds_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_bounds"); + return; + } +} + +native_rectangle_t native_window_get_bounds(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_rectangle_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetBounds(); + return to_c_rectangle(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_bounds"); + native_rectangle_t result = {}; + return result; + } +} + +void native_window_set_content_bounds(native_window_t window, native_rectangle_t bounds) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto bounds_cpp = to_cpp_rectangle(bounds); + self->SetContentBounds(bounds_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_content_bounds"); + return; + } +} + +native_rectangle_t native_window_get_content_bounds(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_rectangle_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetContentBounds(); + return to_c_rectangle(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_content_bounds"); + native_rectangle_t result = {}; + return result; + } +} + +void native_window_set_size(native_window_t window, native_size_t size, bool animate) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto size_cpp = to_cpp_size(size); + self->SetSize(size_cpp, animate); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_size"); + return; + } +} + +native_size_t native_window_get_size(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_size_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetSize(); + return to_c_size(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_size"); + native_size_t result = {}; + return result; + } +} + +void native_window_set_content_size(native_window_t window, native_size_t size) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto size_cpp = to_cpp_size(size); + self->SetContentSize(size_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_content_size"); + return; + } +} + +native_size_t native_window_get_content_size(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_size_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetContentSize(); + return to_c_size(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_content_size"); + native_size_t result = {}; + return result; + } +} + +void native_window_set_minimum_size(native_window_t window, native_size_t size) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto size_cpp = to_cpp_size(size); + self->SetMinimumSize(size_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_minimum_size"); + return; + } +} + +native_size_t native_window_get_minimum_size(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_size_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetMinimumSize(); + return to_c_size(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_minimum_size"); + native_size_t result = {}; + return result; + } +} + +void native_window_set_maximum_size(native_window_t window, native_size_t size) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto size_cpp = to_cpp_size(size); + self->SetMaximumSize(size_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_maximum_size"); + return; + } +} + +native_size_t native_window_get_maximum_size(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_size_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetMaximumSize(); + return to_c_size(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_maximum_size"); + native_size_t result = {}; + return result; + } +} + +void native_window_set_resizable(native_window_t window, bool is_resizable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetResizable(is_resizable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_resizable"); + return; + } +} + +bool native_window_is_resizable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsResizable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_resizable"); + return false; + } +} + +void native_window_set_movable(native_window_t window, bool is_movable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetMovable(is_movable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_movable"); + return; + } +} + +bool native_window_is_movable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsMovable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_movable"); + return false; + } +} + +void native_window_set_minimizable(native_window_t window, bool is_minimizable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetMinimizable(is_minimizable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_minimizable"); + return; + } +} + +bool native_window_is_minimizable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsMinimizable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_minimizable"); + return false; + } +} + +void native_window_set_maximizable(native_window_t window, bool is_maximizable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetMaximizable(is_maximizable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_maximizable"); + return; + } +} + +bool native_window_is_maximizable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsMaximizable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_maximizable"); + return false; + } +} + +void native_window_set_full_screenable(native_window_t window, bool is_full_screenable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetFullScreenable(is_full_screenable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_full_screenable"); + return; + } +} + +bool native_window_is_full_screenable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsFullScreenable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_full_screenable"); + return false; + } +} + +void native_window_set_closable(native_window_t window, bool is_closable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetClosable(is_closable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_closable"); + return; + } +} + +bool native_window_is_closable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsClosable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_closable"); + return false; + } +} + +void native_window_set_window_control_buttons_visible(native_window_t window, bool is_visible) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetWindowControlButtonsVisible(is_visible); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_window_control_buttons_visible"); + return; + } +} + +bool native_window_is_window_control_buttons_visible(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsWindowControlButtonsVisible(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_window_control_buttons_visible"); + return false; + } +} + +void native_window_set_always_on_top(native_window_t window, bool is_always_on_top) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetAlwaysOnTop(is_always_on_top); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_always_on_top"); + return; + } +} + +bool native_window_is_always_on_top(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsAlwaysOnTop(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_always_on_top"); + return false; + } +} + +void native_window_set_position(native_window_t window, native_point_t point) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto point_cpp = to_cpp_point(point); + self->SetPosition(point_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_position"); + return; + } +} + +native_point_t native_window_get_position(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_point_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetPosition(); + return to_c_point(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_position"); + native_point_t result = {}; + return result; + } +} + +void native_window_center(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->Center(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_center"); + return; + } +} + +void native_window_set_title(native_window_t window, const char* title) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetTitle(std::string(title ? title : "")); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_title"); + return; + } +} + +char* native_window_get_title(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return nullptr; + } + try { + return to_c_str(self->GetTitle()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_title"); + return nullptr; + } +} + +void native_window_set_title_bar_style(native_window_t window, native_title_bar_style_t style) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetTitleBarStyle(to_cpp_title_bar_style(style)); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_title_bar_style"); + return; + } +} + +native_title_bar_style_t native_window_get_title_bar_style(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return (native_title_bar_style_t)NATIVE_TITLE_BAR_STYLE_NORMAL; + } + try { + return to_c_title_bar_style(self->GetTitleBarStyle()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_title_bar_style"); + return (native_title_bar_style_t)NATIVE_TITLE_BAR_STYLE_NORMAL; + } +} + +void native_window_set_has_shadow(native_window_t window, bool has_shadow) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetHasShadow(has_shadow); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_has_shadow"); + return; + } +} + +bool native_window_has_shadow(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->HasShadow(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_has_shadow"); + return false; + } +} + +void native_window_set_opacity(native_window_t window, float opacity) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetOpacity(opacity); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_opacity"); + return; + } +} + +float native_window_get_opacity(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return 0; + } + try { + return self->GetOpacity(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_opacity"); + return 0; + } +} + +void native_window_set_visual_effect(native_window_t window, native_visual_effect_t effect) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetVisualEffect(to_cpp_visual_effect(effect)); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_visual_effect"); + return; + } +} + +native_visual_effect_t native_window_get_visual_effect(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return (native_visual_effect_t)NATIVE_VISUAL_EFFECT_NONE; + } + try { + return to_c_visual_effect(self->GetVisualEffect()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_visual_effect"); + return (native_visual_effect_t)NATIVE_VISUAL_EFFECT_NONE; + } +} + +void native_window_set_background_color(native_window_t window, native_color_t color) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + auto color_cpp = to_cpp_color(color); + self->SetBackgroundColor(color_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_background_color"); + return; + } +} + +native_color_t native_window_get_background_color(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + native_color_t result = {}; + return result; + } + try { + const auto cpp_result = self->GetBackgroundColor(); + return to_c_color(cpp_result); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_get_background_color"); + native_color_t result = {}; + return result; + } +} + +void native_window_set_visible_on_all_workspaces(native_window_t window, bool is_visible_on_all_workspaces) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetVisibleOnAllWorkspaces(is_visible_on_all_workspaces); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_visible_on_all_workspaces"); + return; + } +} + +bool native_window_is_visible_on_all_workspaces(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsVisibleOnAllWorkspaces(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_visible_on_all_workspaces"); + return false; + } +} + +void native_window_set_ignore_mouse_events(native_window_t window, bool is_ignore_mouse_events) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetIgnoreMouseEvents(is_ignore_mouse_events); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_ignore_mouse_events"); + return; + } +} + +bool native_window_is_ignore_mouse_events(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsIgnoreMouseEvents(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_ignore_mouse_events"); + return false; + } +} + +void native_window_set_focusable(native_window_t window, bool is_focusable) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->SetFocusable(is_focusable); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_set_focusable"); + return; + } +} + +bool native_window_is_focusable(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return false; + } + try { + return self->IsFocusable(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_is_focusable"); + return false; + } +} + +void native_window_start_dragging(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->StartDragging(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_start_dragging"); + return; + } +} + +void native_window_start_resizing(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return; + } + try { + self->StartResizing(); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_start_resizing"); + return; + } +} + +void* native_window_get_native_object(native_window_t window) { + auto self = nativeapi::HandleTable::GetInstance().Resolve(window); + if (!self) { + return nullptr; + } + return self->GetNativeObject(); +} + +void native_window_free(native_window_t window) { + // The table invalidates the handle itself, so releasing an unknown or + // already-released one is a no-op rather than a double free. + nativeapi::HandleTable::GetInstance().Release(window); +} + +void native_window_list_free(native_window_list_t* list) { + if (!list || !list->windows) { + return; + } + for (long i = 0; i < list->count; ++i) { + nativeapi::HandleTable::GetInstance().Release(list->windows[i]); + } + delete[] list->windows; + list->windows = nullptr; + list->count = 0; +} + +void native_window_list_release(native_window_list_t* list) { + if (!list) { + return; + } + delete[] list->windows; + list->windows = nullptr; + list->count = 0; +} + +bool to_c_window_event(const nativeapi::WindowEvent& event, native_window_event_t* out) { + if (!out) { + return false; + } + *out = native_window_event_t{}; + out->window_id = event.GetWindowId(); + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_FOCUSED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_BLURRED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_MINIMIZED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_MAXIMIZED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_RESTORED; + (void)typed; + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_MOVED; + out->data.moved.new_position = to_c_point(typed->GetNewPosition()); + return true; + } + if (const auto* typed = dynamic_cast(&event)) { + out->type = NATIVE_WINDOW_EVENT_TYPE_RESIZED; + out->data.resized.new_size = to_c_size(typed->GetNewSize()); + return true; + } + return false; +} + +void free_c_window_event(native_window_event_t* value) { + if (!value) { + return; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/window_c.h b/packages/cnativeapi/cxx_impl/src/capi/window_c.h new file mode 100644 index 0000000..1b77867 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/window_c.h @@ -0,0 +1,391 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "color_c.h" +#include "geometry_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int native_window_id_t; + +typedef enum { + NATIVE_TITLE_BAR_STYLE_NORMAL = 0, + NATIVE_TITLE_BAR_STYLE_HIDDEN = 1, +} native_title_bar_style_t; + +typedef enum { + NATIVE_VISUAL_EFFECT_NONE = 0, + NATIVE_VISUAL_EFFECT_BLUR = 1, + NATIVE_VISUAL_EFFECT_ACRYLIC = 2, + NATIVE_VISUAL_EFFECT_MICA = 3, +} native_visual_effect_t; + +/// Opaque Window handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_WINDOW rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef uint64_t native_window_t; + +/// Never refers to a live Window. +#define NATIVE_INVALID_WINDOW ((native_window_t)0) + +/// Owning list of Window handles. +typedef struct { + native_window_t* windows; + long count; +} native_window_list_t; + +/// Which concrete WindowEvent arrived. +typedef enum { + NATIVE_WINDOW_EVENT_TYPE_FOCUSED = 0, + NATIVE_WINDOW_EVENT_TYPE_BLURRED = 1, + NATIVE_WINDOW_EVENT_TYPE_MINIMIZED = 2, + NATIVE_WINDOW_EVENT_TYPE_MAXIMIZED = 3, + NATIVE_WINDOW_EVENT_TYPE_RESTORED = 4, + NATIVE_WINDOW_EVENT_TYPE_MOVED = 5, + NATIVE_WINDOW_EVENT_TYPE_RESIZED = 6, +} native_window_event_type_t; + +/// One WindowEvent, tagged by its concrete type. +/// +/// Valid only for the duration of the callback: anything it points at +/// is released as soon as the callback returns. Copy what you need. +typedef struct { + native_window_event_type_t type; + native_window_id_t window_id; + union { + struct { + native_point_t new_position; + } moved; + struct { + native_size_t new_size; + } resized; + } data; +} native_window_event_t; + +typedef void (*native_window_event_callback_t)(const native_window_event_t* event, void* user_data); + +/// Creates a Window instance; release it with native_window_free(). +FFI_PLUGIN_EXPORT +native_window_t native_window_create(void); + +/// Creates a Window instance; release it with native_window_free(). +FFI_PLUGIN_EXPORT +native_window_t native_window_create_with_native_window(void* native_window); + +FFI_PLUGIN_EXPORT +native_window_id_t native_window_get_id(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_focus(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_blur(native_window_t window); + +FFI_PLUGIN_EXPORT +bool native_window_is_focused(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_show(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_show_inactive(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_hide(native_window_t window); + +FFI_PLUGIN_EXPORT +bool native_window_is_visible(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_maximize(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_unmaximize(native_window_t window); + +FFI_PLUGIN_EXPORT +bool native_window_is_maximized(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_minimize(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_restore(native_window_t window); + +FFI_PLUGIN_EXPORT +bool native_window_is_minimized(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_full_screen(native_window_t window, bool is_full_screen); + +FFI_PLUGIN_EXPORT +bool native_window_is_full_screen(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_bounds(native_window_t window, native_rectangle_t bounds); + +FFI_PLUGIN_EXPORT +native_rectangle_t native_window_get_bounds(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_content_bounds(native_window_t window, native_rectangle_t bounds); + +FFI_PLUGIN_EXPORT +native_rectangle_t native_window_get_content_bounds(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_size(native_window_t window, native_size_t size, bool animate); + +FFI_PLUGIN_EXPORT +native_size_t native_window_get_size(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_content_size(native_window_t window, native_size_t size); + +FFI_PLUGIN_EXPORT +native_size_t native_window_get_content_size(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_minimum_size(native_window_t window, native_size_t size); + +FFI_PLUGIN_EXPORT +native_size_t native_window_get_minimum_size(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_maximum_size(native_window_t window, native_size_t size); + +FFI_PLUGIN_EXPORT +native_size_t native_window_get_maximum_size(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_resizable(native_window_t window, bool is_resizable); + +FFI_PLUGIN_EXPORT +bool native_window_is_resizable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_movable(native_window_t window, bool is_movable); + +FFI_PLUGIN_EXPORT +bool native_window_is_movable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_minimizable(native_window_t window, bool is_minimizable); + +FFI_PLUGIN_EXPORT +bool native_window_is_minimizable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_maximizable(native_window_t window, bool is_maximizable); + +FFI_PLUGIN_EXPORT +bool native_window_is_maximizable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_full_screenable(native_window_t window, bool is_full_screenable); + +FFI_PLUGIN_EXPORT +bool native_window_is_full_screenable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_closable(native_window_t window, bool is_closable); + +FFI_PLUGIN_EXPORT +bool native_window_is_closable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_window_control_buttons_visible(native_window_t window, bool is_visible); + +FFI_PLUGIN_EXPORT +bool native_window_is_window_control_buttons_visible(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_always_on_top(native_window_t window, bool is_always_on_top); + +FFI_PLUGIN_EXPORT +bool native_window_is_always_on_top(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_position(native_window_t window, native_point_t point); + +FFI_PLUGIN_EXPORT +native_point_t native_window_get_position(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_center(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_title(native_window_t window, const char* title); + +/// Caller owns the returned string; free it with free_c_str(). +FFI_PLUGIN_EXPORT +char* native_window_get_title(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_title_bar_style(native_window_t window, native_title_bar_style_t style); + +FFI_PLUGIN_EXPORT +native_title_bar_style_t native_window_get_title_bar_style(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_has_shadow(native_window_t window, bool has_shadow); + +FFI_PLUGIN_EXPORT +bool native_window_has_shadow(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_opacity(native_window_t window, float opacity); + +FFI_PLUGIN_EXPORT +float native_window_get_opacity(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_visual_effect(native_window_t window, native_visual_effect_t effect); + +FFI_PLUGIN_EXPORT +native_visual_effect_t native_window_get_visual_effect(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_background_color(native_window_t window, native_color_t color); + +FFI_PLUGIN_EXPORT +native_color_t native_window_get_background_color(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_visible_on_all_workspaces(native_window_t window, bool is_visible_on_all_workspaces); + +FFI_PLUGIN_EXPORT +bool native_window_is_visible_on_all_workspaces(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_ignore_mouse_events(native_window_t window, bool is_ignore_mouse_events); + +FFI_PLUGIN_EXPORT +bool native_window_is_ignore_mouse_events(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_set_focusable(native_window_t window, bool is_focusable); + +FFI_PLUGIN_EXPORT +bool native_window_is_focusable(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_start_dragging(native_window_t window); + +FFI_PLUGIN_EXPORT +void native_window_start_resizing(native_window_t window); + +/// Platform-specific native object (NSScreen*, HMONITOR, ...). +FFI_PLUGIN_EXPORT +void* native_window_get_native_object(native_window_t window); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +FFI_PLUGIN_EXPORT +void native_window_free(native_window_t window); + +/// Frees the array and releases every handle it contains. +FFI_PLUGIN_EXPORT +void native_window_list_free(native_window_list_t* list); + +/// Frees only the array; the caller takes over the handles. +FFI_PLUGIN_EXPORT +void native_window_list_release(native_window_list_t* list); + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +namespace nativeapi { +class WindowEvent; +} // namespace nativeapi + +/// Fills @p out from @p event. Returns false when the event is not one +/// of the concrete types the C ABI knows about. +bool to_c_window_event(const nativeapi::WindowEvent& event, native_window_event_t* out); +/// Releases everything to_c_window_event() allocated. +void free_c_window_event(native_window_event_t* value); + +#endif + +#ifdef __cplusplus +#include "../window.h" +#include "string_utils_c.h" + +// Conversion helpers between these C types and their C++ originals. + +inline native_title_bar_style_t to_c_title_bar_style(nativeapi::TitleBarStyle value); +inline nativeapi::TitleBarStyle to_cpp_title_bar_style(native_title_bar_style_t value); +inline native_visual_effect_t to_c_visual_effect(nativeapi::VisualEffect value); +inline nativeapi::VisualEffect to_cpp_visual_effect(native_visual_effect_t value); + +inline native_title_bar_style_t to_c_title_bar_style(nativeapi::TitleBarStyle value) { + switch (value) { + case nativeapi::TitleBarStyle::Normal: + return NATIVE_TITLE_BAR_STYLE_NORMAL; + case nativeapi::TitleBarStyle::Hidden: + return NATIVE_TITLE_BAR_STYLE_HIDDEN; + default: + return NATIVE_TITLE_BAR_STYLE_NORMAL; + } +} + +inline nativeapi::TitleBarStyle to_cpp_title_bar_style(native_title_bar_style_t value) { + switch (value) { + case NATIVE_TITLE_BAR_STYLE_NORMAL: + return nativeapi::TitleBarStyle::Normal; + case NATIVE_TITLE_BAR_STYLE_HIDDEN: + return nativeapi::TitleBarStyle::Hidden; + default: + return nativeapi::TitleBarStyle::Normal; + } +} + +inline native_visual_effect_t to_c_visual_effect(nativeapi::VisualEffect value) { + switch (value) { + case nativeapi::VisualEffect::None: + return NATIVE_VISUAL_EFFECT_NONE; + case nativeapi::VisualEffect::Blur: + return NATIVE_VISUAL_EFFECT_BLUR; + case nativeapi::VisualEffect::Acrylic: + return NATIVE_VISUAL_EFFECT_ACRYLIC; + case nativeapi::VisualEffect::Mica: + return NATIVE_VISUAL_EFFECT_MICA; + default: + return NATIVE_VISUAL_EFFECT_NONE; + } +} + +inline nativeapi::VisualEffect to_cpp_visual_effect(native_visual_effect_t value) { + switch (value) { + case NATIVE_VISUAL_EFFECT_NONE: + return nativeapi::VisualEffect::None; + case NATIVE_VISUAL_EFFECT_BLUR: + return nativeapi::VisualEffect::Blur; + case NATIVE_VISUAL_EFFECT_ACRYLIC: + return nativeapi::VisualEffect::Acrylic; + case NATIVE_VISUAL_EFFECT_MICA: + return nativeapi::VisualEffect::Mica; + default: + return nativeapi::VisualEffect::None; + } +} + +#endif // __cplusplus diff --git a/packages/cnativeapi/cxx_impl/src/capi/window_manager_c.cpp b/packages/cnativeapi/cxx_impl/src/capi/window_manager_c.cpp new file mode 100644 index 0000000..1e0fbad --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/window_manager_c.cpp @@ -0,0 +1,213 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "window_manager_c.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils_c.h" +#include "../foundation/handle_table.h" +#include "../window.h" +#include "window_c.h" +#include "../window_manager.h" + +native_window_t native_window_manager_get(native_window_id_t id) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::WindowManager::GetInstance().Get(id)); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_get"); + return 0; + } +} + +native_window_list_t native_window_manager_get_all(void) { + try { + const auto items = nativeapi::WindowManager::GetInstance().GetAll(); + native_window_list_t list = {}; + if (items.empty()) { + return list; + } + list.windows = new (std::nothrow) native_window_t[items.size()]; + if (!list.windows) { + return list; + } + for (size_t i = 0; i < items.size(); ++i) { + list.windows[i] = nativeapi::HandleTable::GetInstance().Insert(items[i]); + } + list.count = static_cast(items.size()); + return list; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_get_all"); + native_window_list_t empty = {}; + return empty; + } +} + +native_window_t native_window_manager_get_current(void) { + try { + return nativeapi::HandleTable::GetInstance().Insert(nativeapi::WindowManager::GetInstance().GetCurrent()); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_get_current"); + return 0; + } +} + +void native_window_manager_set_will_show_hook(native_window_manager_set_will_show_hook_callback_t hook, void* hook_user_data) { + try { + std::optional> hook_cpp; + if (hook) { + hook_cpp = [hook, hook_user_data](unsigned int arg0) { hook(arg0, hook_user_data); }; + } + nativeapi::WindowManager::GetInstance().SetWillShowHook(hook_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_set_will_show_hook"); + return; + } +} + +void native_window_manager_set_will_hide_hook(native_window_manager_set_will_hide_hook_callback_t hook, void* hook_user_data) { + try { + std::optional> hook_cpp; + if (hook) { + hook_cpp = [hook, hook_user_data](unsigned int arg0) { hook(arg0, hook_user_data); }; + } + nativeapi::WindowManager::GetInstance().SetWillHideHook(hook_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_set_will_hide_hook"); + return; + } +} + +void native_window_manager_set_will_close_hook(native_window_manager_set_will_close_hook_callback_t hook, void* hook_user_data) { + try { + std::optional> hook_cpp; + if (hook) { + hook_cpp = [hook, hook_user_data](unsigned int arg0) { hook(arg0, hook_user_data); }; + } + nativeapi::WindowManager::GetInstance().SetWillCloseHook(hook_cpp); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_set_will_close_hook"); + return; + } +} + +bool native_window_manager_has_will_show_hook(void) { + try { + return nativeapi::WindowManager::GetInstance().HasWillShowHook(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_has_will_show_hook"); + return false; + } +} + +bool native_window_manager_has_will_hide_hook(void) { + try { + return nativeapi::WindowManager::GetInstance().HasWillHideHook(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_has_will_hide_hook"); + return false; + } +} + +bool native_window_manager_has_will_close_hook(void) { + try { + return nativeapi::WindowManager::GetInstance().HasWillCloseHook(); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_has_will_close_hook"); + return false; + } +} + +void native_window_manager_handle_will_show(native_window_id_t id) { + try { + nativeapi::WindowManager::GetInstance().HandleWillShow(id); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_handle_will_show"); + return; + } +} + +void native_window_manager_handle_will_hide(native_window_id_t id) { + try { + nativeapi::WindowManager::GetInstance().HandleWillHide(id); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_handle_will_hide"); + return; + } +} + +void native_window_manager_handle_will_close(native_window_id_t id) { + try { + nativeapi::WindowManager::GetInstance().HandleWillClose(id); + return; + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_handle_will_close"); + return; + } +} + +bool native_window_manager_call_original_show(native_window_id_t id) { + try { + return nativeapi::WindowManager::GetInstance().CallOriginalShow(id); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_call_original_show"); + return false; + } +} + +bool native_window_manager_call_original_hide(native_window_id_t id) { + try { + return nativeapi::WindowManager::GetInstance().CallOriginalHide(id); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_call_original_hide"); + return false; + } +} + +bool native_window_manager_call_original_close(native_window_id_t id) { + try { + return nativeapi::WindowManager::GetInstance().CallOriginalClose(id); + } catch (...) { + fprintf(stderr, "[nativeapi] %s: unexpected exception\n", "native_window_manager_call_original_close"); + return false; + } +} + +native_listener_id_t native_window_manager_add_listener(native_window_event_callback_t callback, void* user_data) { + if (!callback) { + return 0; + } + try { + return static_cast(nativeapi::WindowManager::GetInstance().AddListener( + [callback, user_data](const nativeapi::WindowEvent& event) { + native_window_event_t c_event = {}; + if (!to_c_window_event(event, &c_event)) { + return; + } + callback(&c_event, user_data); + free_c_window_event(&c_event); + })); + } catch (...) { + return 0; + } +} + +bool native_window_manager_remove_listener(native_listener_id_t listener_id) { + try { + return nativeapi::WindowManager::GetInstance().RemoveListener(static_cast(listener_id)); + } catch (...) { + return false; + } +} + diff --git a/packages/cnativeapi/cxx_impl/src/capi/window_manager_c.h b/packages/cnativeapi/cxx_impl/src/capi/window_manager_c.h new file mode 100644 index 0000000..257b9ed --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/capi/window_manager_c.h @@ -0,0 +1,86 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#pragma once + +#include +#include + +#include "common_c.h" +#include "window_c.h" + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*native_window_manager_set_will_show_hook_callback_t)(unsigned int arg0, void* user_data); + +typedef void (*native_window_manager_set_will_hide_hook_callback_t)(unsigned int arg0, void* user_data); + +typedef void (*native_window_manager_set_will_close_hook_callback_t)(unsigned int arg0, void* user_data); + +/// Caller owns the returned handle; release it with native_window_free(). +FFI_PLUGIN_EXPORT +native_window_t native_window_manager_get(native_window_id_t id); + +FFI_PLUGIN_EXPORT +native_window_list_t native_window_manager_get_all(void); + +/// Caller owns the returned handle; release it with native_window_free(). +FFI_PLUGIN_EXPORT +native_window_t native_window_manager_get_current(void); + +FFI_PLUGIN_EXPORT +void native_window_manager_set_will_show_hook(native_window_manager_set_will_show_hook_callback_t hook, void* hook_user_data); + +FFI_PLUGIN_EXPORT +void native_window_manager_set_will_hide_hook(native_window_manager_set_will_hide_hook_callback_t hook, void* hook_user_data); + +FFI_PLUGIN_EXPORT +void native_window_manager_set_will_close_hook(native_window_manager_set_will_close_hook_callback_t hook, void* hook_user_data); + +FFI_PLUGIN_EXPORT +bool native_window_manager_has_will_show_hook(void); + +FFI_PLUGIN_EXPORT +bool native_window_manager_has_will_hide_hook(void); + +FFI_PLUGIN_EXPORT +bool native_window_manager_has_will_close_hook(void); + +FFI_PLUGIN_EXPORT +void native_window_manager_handle_will_show(native_window_id_t id); + +FFI_PLUGIN_EXPORT +void native_window_manager_handle_will_hide(native_window_id_t id); + +FFI_PLUGIN_EXPORT +void native_window_manager_handle_will_close(native_window_id_t id); + +FFI_PLUGIN_EXPORT +bool native_window_manager_call_original_show(native_window_id_t id); + +FFI_PLUGIN_EXPORT +bool native_window_manager_call_original_hide(native_window_id_t id); + +FFI_PLUGIN_EXPORT +bool native_window_manager_call_original_close(native_window_id_t id); + +/// Registers @p callback for every WindowEvent this WindowManager emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +FFI_PLUGIN_EXPORT +native_listener_id_t native_window_manager_add_listener(native_window_event_callback_t callback, void* user_data); + +/// Unregisters a listener. Returns false if unknown. +FFI_PLUGIN_EXPORT +bool native_window_manager_remove_listener(native_listener_id_t listener_id); + +#ifdef __cplusplus +} +#endif diff --git a/packages/cnativeapi/cxx_impl/src/dialog.cpp b/packages/cnativeapi/cxx_impl/src/dialog.cpp new file mode 100644 index 0000000..719e1b3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/dialog.cpp @@ -0,0 +1,15 @@ +#include "dialog.h" + +namespace nativeapi { + +Dialog::~Dialog() = default; + +bool Dialog::Open() { + return false; // Base class implementation - should be overridden +} + +bool Dialog::Close() { + return false; // Base class implementation - should be overridden +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/dialog.h b/packages/cnativeapi/cxx_impl/src/dialog.h new file mode 100644 index 0000000..b019501 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/dialog.h @@ -0,0 +1,136 @@ +#pragma once + +#include + +namespace nativeapi { + +/** + * @enum DialogModality + * @brief Dialog modality types. + * + * Defines how the dialog blocks user interaction. + */ +enum class DialogModality { + /** + * @brief None - Non-modal dialog. + * + * The dialog does not block user interaction. The application continues + * to run and users can interact with other windows while the dialog is open. + */ + None, + + /** + * @brief Application - Blocks the current application. + * + * Blocks interaction with all windows in the current application, + * but allows interaction with other applications. + */ + Application, + + /** + * @brief Window - Blocks the parent window (requires parent window handle). + * + * Blocks interaction with a specific parent window. + * Requires a parent window handle to be provided. + */ + Window +}; + +/** + * @class Dialog + * @brief Base class for all dialog types. + * + * This abstract class provides the common interface for all dialog types + * in the system. Specific dialog types (MessageDialog, FileDialog, etc.) + * inherit from this class and implement their specific behavior. + * + * The Dialog class provides: + * - Modal and non-modal display modes + * - Modal state management + * + * @note This is an abstract base class. Use specific dialog types like + * MessageDialog, FileDialog, etc., to create actual dialogs. + * + * @example + * ```cpp + * // Create a message dialog (see MessageDialog for details) + * auto message_dialog = std::make_shared( + * "Title", "Message", MessageDialogType::Info); + * + * // Set modal mode and open + * message_dialog->SetModality(DialogModality::Application); + * message_dialog->Open(); + * ``` + */ +class Dialog { + public: + /** + * @brief Virtual destructor. + * + * Ensures proper cleanup of derived classes and platform-specific resources. + */ + virtual ~Dialog(); + + /** + * @brief Get the current modality setting of the dialog. + * + * @return The current DialogModality setting + */ + virtual DialogModality GetModality() const = 0; + + /** + * @brief Set the modality of the dialog. + * + * The modality determines how the dialog blocks user interaction: + * - None: Non-modal dialog, does not block user interaction + * - Application: Blocks interaction with all windows in the current application + * - Window: Blocks interaction with a specific parent window (requires parent handle) + * + * @param modality The modality type to set + * + * @note This setting affects the behavior when Open() is called. + * The modality should be set before opening the dialog. + * + * @example + * ```cpp + * dialog->SetModality(DialogModality::Application); // Make it application modal + * dialog->Open(); // Open as modal dialog + * ``` + */ + virtual void SetModality(DialogModality modality) = 0; + + /** + * @brief Open the dialog according to its modality setting. + * + * The dialog behavior depends on the current modality: + * - None: Opens non-modally, does not block the calling thread + * - Application/Window: Opens modally, blocks until the user dismisses the dialog + * + * @return true if the dialog was successfully opened, false otherwise + * + * @example + * ```cpp + * // Non-modal dialog + * dialog->SetModality(DialogModality::None); + * dialog->Open(); + * // Application continues running... + * + * // Modal dialog + * dialog->SetModality(DialogModality::Application); + * dialog->Open(); + * // Blocks until user dismisses dialog + * ``` + */ + virtual bool Open(); + + /** + * @brief Close the dialog programmatically. + * + * Dismisses the dialog as if the user had closed it. + * + * @return true if the dialog was successfully closed, false otherwise + */ + virtual bool Close(); +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/display.h b/packages/cnativeapi/cxx_impl/src/display.h new file mode 100644 index 0000000..5616a1f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/display.h @@ -0,0 +1,190 @@ +#pragma once +#include +#include +#include "foundation/event.h" +#include "foundation/geometry.h" +#include "foundation/id_allocator.h" +#include "foundation/native_object_provider.h" + +namespace nativeapi { + +/** + * @typedef DisplayId + * @brief Unique identifier for a display instance. + * + * Allocated from IdAllocator when the Display object is created. Stable for + * as long as the display stays connected: DisplayManager hands out the same + * Display instance (and therefore the same id) on every enumeration. A + * display that is disconnected and reconnected gets a fresh instance with a + * fresh id. + */ +typedef IdAllocator::IdType DisplayId; + +/** + * Display orientation enumeration + */ +enum class DisplayOrientation { + kPortrait = 0, + kLandscape = 90, + kPortraitFlipped = 180, + kLandscapeFlipped = 270 +}; + +/** + * Representation of a display/monitor. + * + * Display is an identity object: it stands for one + * physical display, is managed through std::shared_ptr, and is identified by + * an integer DisplayId. Instances are created and cached by DisplayManager — + * asking the manager twice for the same physical display returns the same + * Display object. Properties are read live from the underlying platform + * display, so a held instance always reflects the current configuration. + * + * Display is not copyable. Share the std::shared_ptr instead. + */ +class Display : public NativeObjectProvider { + public: + /** + * @brief Constructor that wraps an existing native display object. + * + * @param display Pointer to the platform-specific display object + * (NSScreen* on macOS, HMONITOR on Windows, GdkMonitor* on + * Linux). The native object stays owned by the platform. + * + * @note Prefer obtaining displays from DisplayManager, which deduplicates + * instances; construct one directly only to wrap a native object you + * already hold. + */ + explicit Display(void* display); + + Display(const Display&) = delete; + Display& operator=(const Display&) = delete; + Display(Display&&) = delete; + Display& operator=(Display&&) = delete; + + virtual ~Display(); + + // Basic identification + DisplayId GetId() const; + std::string GetName() const; + + // Physical properties + Point GetPosition() const; + Size GetSize() const; + Rectangle GetWorkArea() const; + double GetScaleFactor() const; + + // Additional properties + bool IsPrimary() const; + DisplayOrientation GetOrientation() const; + int GetRefreshRate() const; + int GetBitDepth() const; + + protected: + /** + * @brief Internal method to get the platform-specific native display object. + * + * This method must be implemented by platform-specific code to return + * the underlying native display object. + * + * @return Pointer to the native display object + */ + void* GetNativeObjectInternal() const override; + + private: + class Impl; + std::unique_ptr pimpl_; +}; + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * Base class for all display-related events + * + * This class provides common functionality for display events, + * including access to the display that triggered the event. + */ +class DisplayEvent : public Event { + public: + /** + * Constructor for DisplayEvent + * @param display The display associated with this event + */ + explicit DisplayEvent(std::shared_ptr display) : display_(std::move(display)) {} + + /** + * Virtual destructor + */ + virtual ~DisplayEvent() = default; + + /** + * Get the display associated with this event + * @return Shared pointer to the display + */ + std::shared_ptr GetDisplay() const { return display_; } + + /** + * Get a string representation of the event type (for debugging) + * Default implementation returns "DisplayEvent" + */ + std::string GetTypeName() const override { return "DisplayEvent"; } + + private: + std::shared_ptr display_; +}; + +/** + * Event class for display addition + * + * This event is emitted when a new display is connected to the system. + */ +class DisplayAddedEvent : public DisplayEvent { + public: + explicit DisplayAddedEvent(std::shared_ptr display) + : DisplayEvent(std::move(display)) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "DisplayAddedEvent"; } +}; + +/** + * Event class for display removal + * + * This event is emitted when a display is disconnected from the system. + * The carried Display instance is the last reference to the now-disconnected + * display; its id is no longer resolvable through DisplayManager. + */ +class DisplayRemovedEvent : public DisplayEvent { + public: + explicit DisplayRemovedEvent(std::shared_ptr display) + : DisplayEvent(std::move(display)) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "DisplayRemovedEvent"; } +}; + +/** + * Event class for display configuration changes + * + * This event is emitted when a display's properties change (resolution, + * orientation, etc.). Displays are identity objects whose properties are read + * live, so the carried instance already reflects the new configuration. + */ +class DisplayChangedEvent : public DisplayEvent { + public: + explicit DisplayChangedEvent(std::shared_ptr display) + : DisplayEvent(std::move(display)) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "DisplayChangedEvent"; } +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/display_manager.cpp b/packages/cnativeapi/cxx_impl/src/display_manager.cpp new file mode 100644 index 0000000..c7813c6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/display_manager.cpp @@ -0,0 +1,76 @@ +#include "display_manager.h" + +#include + +namespace nativeapi { + +DisplayManager& DisplayManager::GetInstance() { + static DisplayManager instance; + return instance; +} + +std::vector> DisplayManager::GetAll() { + return Reconcile(EnumerateNativeDisplays(), nullptr, nullptr); +} + +std::shared_ptr DisplayManager::GetPrimary() { + auto natives = EnumerateNativeDisplays(); + auto displays = Reconcile(natives, nullptr, nullptr); + for (size_t i = 0; i < natives.size(); ++i) { + if (natives[i].is_primary) { + return displays[i]; + } + } + return displays.empty() ? nullptr : displays.front(); +} + +std::vector> DisplayManager::Reconcile( + const std::vector& natives, + std::vector>* added, + std::vector>* removed) { + std::vector> current; + current.reserve(natives.size()); + + std::unordered_map> next; + next.reserve(natives.size()); + + for (const auto& native : natives) { + auto it = displays_.find(native.key); + std::shared_ptr display; + if (it != displays_.end()) { + display = it->second; + } else { + display = std::make_shared(native.native); + if (added) { + added->push_back(display); + } + } + next.emplace(native.key, display); + current.push_back(std::move(display)); + } + + if (removed) { + for (const auto& entry : displays_) { + if (next.find(entry.first) == next.end()) { + removed->push_back(entry.second); + } + } + } + + displays_ = std::move(next); + return current; +} + +void DisplayManager::HandleDisplaysChanged() { + std::vector> added; + std::vector> removed; + Reconcile(EnumerateNativeDisplays(), &added, &removed); + for (const auto& display : added) { + Emit(display); + } + for (const auto& display : removed) { + Emit(display); + } +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/display_manager.h b/packages/cnativeapi/cxx_impl/src/display_manager.h new file mode 100644 index 0000000..ce1b195 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/display_manager.h @@ -0,0 +1,152 @@ +#pragma once + +#include +#include +#include +#include + +#include "display.h" +#include "foundation/event.h" +#include "foundation/event_emitter.h" +#include "foundation/geometry.h" + +namespace nativeapi { + +/** + * DisplayManager is a singleton that manages all displays on the system. + * + * This class provides functionality to: + * - Query all connected displays + * - Get primary display information + * - Monitor display changes (addition/removal) + * - Get cursor position across displays + * + * Display is an identity object: the manager keeps + * one live Display instance per connected physical display and returns the + * same std::shared_ptr on every query, so a display's DisplayId stays stable + * for as long as it remains connected. + * + * Thread Safety: This class is not thread-safe. External synchronization + * is required if accessed from multiple threads. + * + * Example usage: + * @code + * DisplayManager& manager = DisplayManager::GetInstance(); + * std::vector> displays = manager.GetAll(); + * std::shared_ptr primary = manager.GetPrimary(); + * @endcode + */ +class DisplayManager : public EventEmitter { + public: + /** + * Get the singleton instance of DisplayManager + * @return Reference to the singleton DisplayManager instance + */ + static DisplayManager& GetInstance(); + + /** + * @brief Destructor for DisplayManager. + * + * Cleans up all resources, stops event monitoring. + * This is automatically called when the application terminates. + */ + virtual ~DisplayManager(); + + /** + * Get all connected displays + * + * @return Vector of shared pointers to all connected displays. The vector + * may be empty if no displays are detected. Repeated calls return the same + * Display instances for displays that stayed connected. + */ + std::vector> GetAll(); + + /** + * Get the primary display + * + * The primary display is typically the main screen where the desktop + * environment displays its primary interface elements. + * + * @return Shared pointer to the primary display, or nullptr if no display + * is available. + */ + std::shared_ptr GetPrimary(); + + /** + * Get the current cursor position in screen coordinates + * + * The coordinates are relative to the top-left corner of the primary display, + * with positive X extending right and positive Y extending down. + * + * @return Point containing the current cursor coordinates (x, y) + * @note The position is captured at the time of the function call + */ + Point GetCursorPosition(); + + // Prevent copy construction and assignment to maintain singleton property + DisplayManager(const DisplayManager&) = delete; + DisplayManager& operator=(const DisplayManager&) = delete; + DisplayManager(DisplayManager&&) = delete; + DisplayManager& operator=(DisplayManager&&) = delete; + + private: + /** + * @brief Private constructor to enforce singleton pattern. + * + * Initializes the DisplayManager instance and sets up platform display + * change monitoring. + */ + DisplayManager(); + + /** + * One display as reported by the platform enumeration. + */ + struct NativeDisplayInfo { + /** + * Platform-stable identity key (e.g. CGDirectDisplayID on macOS, device + * name on Windows). Used to recognize an already-known display across + * enumerations; never exposed publicly. + */ + std::string key; + + /** Platform display object, consumable by Display's constructor. */ + void* native; + + /** Whether the platform reports this display as primary. */ + bool is_primary; + }; + + /** + * Enumerate the platform's current displays. Implemented per platform; + * everything else (instance caching, diffing, events) is shared code. + */ + std::vector EnumerateNativeDisplays(); + + /** + * Reconcile the instance cache against a platform enumeration. + * + * Known displays keep their existing instance; new ones get a fresh + * Display; missing ones are dropped from the cache. When @p added / + * @p removed are non-null they receive the corresponding instances. + * + * @return The current displays in enumeration order. + */ + std::vector> Reconcile( + const std::vector& natives, + std::vector>* added, + std::vector>* removed); + + /** + * Re-enumerate and emit DisplayAddedEvent / DisplayRemovedEvent for the + * differences. Called by the platform display-change observers. + */ + void HandleDisplaysChanged(); + + /** + * Live Display instances keyed by platform identity key, so repeated + * enumeration returns the same objects (stable DisplayId). + */ + std::unordered_map> displays_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/color.cpp b/packages/cnativeapi/cxx_impl/src/foundation/color.cpp new file mode 100644 index 0000000..36f990e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/color.cpp @@ -0,0 +1,86 @@ +#include "color.h" +#include +#include + +namespace nativeapi { + +// Parse a single hex digit (0-9, A-F, a-f) to a value 0-15 +static unsigned char ParseHexDigit(char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + throw std::invalid_argument("Invalid hex digit"); +} + +// Parse two hex digits to a byte value 0-255 +static unsigned char ParseHexByte(const char* hex) { + return (ParseHexDigit(hex[0]) << 4) | ParseHexDigit(hex[1]); +} + +Color Color::FromHex(const char* hex) { + if (!hex) { + throw std::invalid_argument("Hex string cannot be null"); + } + + // Skip leading '#' if present + if (hex[0] == '#') { + hex++; + } + + size_t len = std::strlen(hex); + + // Parse based on format + if (len == 3) { + // #RGB format - expand each digit to two digits + unsigned char r = ParseHexDigit(hex[0]); + unsigned char g = ParseHexDigit(hex[1]); + unsigned char b = ParseHexDigit(hex[2]); + // Expand: F -> FF (15 -> 255) + r = (r << 4) | r; + g = (g << 4) | g; + b = (b << 4) | b; + return Color{r, g, b, 255}; + } else if (len == 4) { + // #RGBA format - expand each digit to two digits + unsigned char r = ParseHexDigit(hex[0]); + unsigned char g = ParseHexDigit(hex[1]); + unsigned char b = ParseHexDigit(hex[2]); + unsigned char a = ParseHexDigit(hex[3]); + r = (r << 4) | r; + g = (g << 4) | g; + b = (b << 4) | b; + a = (a << 4) | a; + return Color{r, g, b, a}; + } else if (len == 6) { + // #RRGGBB format + unsigned char r = ParseHexByte(hex); + unsigned char g = ParseHexByte(hex + 2); + unsigned char b = ParseHexByte(hex + 4); + return Color{r, g, b, 255}; + } else if (len == 8) { + // #RRGGBBAA format + unsigned char r = ParseHexByte(hex); + unsigned char g = ParseHexByte(hex + 2); + unsigned char b = ParseHexByte(hex + 4); + unsigned char a = ParseHexByte(hex + 6); + return Color{r, g, b, a}; + } else { + throw std::invalid_argument("Invalid hex color format. Expected #RGB, #RGBA, #RRGGBB, or #RRGGBBAA"); + } +} + +// Color constants +const Color Color::Transparent = {0, 0, 0, 0}; +const Color Color::Black = {0, 0, 0, 255}; +const Color Color::White = {255, 255, 255, 255}; +const Color Color::Red = {255, 0, 0, 255}; +const Color Color::Green = {0, 255, 0, 255}; +const Color Color::Blue = {0, 0, 255, 255}; +const Color Color::Yellow = {255, 255, 0, 255}; +const Color Color::Cyan = {0, 255, 255, 255}; +const Color Color::Magenta = {255, 0, 255, 255}; + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/foundation/color.h b/packages/cnativeapi/cxx_impl/src/foundation/color.h new file mode 100644 index 0000000..e3c0503 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/color.h @@ -0,0 +1,101 @@ +#pragma once + +namespace nativeapi { + +/** + * @struct Color + * @brief Represents an RGBA color. + * + * This structure defines a color using red, green, blue, and alpha (transparency) + * components. Each component is represented as an unsigned byte (0-255). + * + * @note Alpha value: 0 = fully transparent, 255 = fully opaque + */ +struct Color { + unsigned char r; ///< Red component (0-255) + unsigned char g; ///< Green component (0-255) + unsigned char b; ///< Blue component (0-255) + unsigned char a; ///< Alpha component (0-255) + + /** + * @brief Creates a Color from RGBA values. + * + * @param red Red component (0-255) + * @param green Green component (0-255) + * @param blue Blue component (0-255) + * @param alpha Alpha component (0-255), defaults to 255 (fully opaque) + * @return Color instance with specified values + * + * @example + * // Create an opaque red color + * auto red = Color::FromRGBA(255, 0, 0); + * + * // Create a semi-transparent blue color + * auto blue = Color::FromRGBA(0, 0, 255, 128); + */ + static Color FromRGBA(unsigned char red, unsigned char green, + unsigned char blue, unsigned char alpha = 255) { + return Color{red, green, blue, alpha}; + } + + /** + * @brief Creates a Color from a hexadecimal string. + * + * Supports multiple hex color formats: + * - "#RGB" - 3-digit hex (e.g., "#F00" = red) + * - "#RGBA" - 4-digit hex with alpha + * - "#RRGGBB" - 6-digit hex (e.g., "#FF0000" = red) + * - "#RRGGBBAA" - 8-digit hex with alpha + * + * @param hex Hexadecimal color string (with or without '#' prefix) + * @return Color instance parsed from the hex string + * + * @example + * // Create a red color + * auto red = Color::FromHex("#FF0000"); + * + * // Create a semi-transparent blue color + * auto blue = Color::FromHex("#0000FF80"); + * + * // Short format + * auto green = Color::FromHex("#0F0"); + */ + static Color FromHex(const char* hex); + + /** + * @brief Converts the color to a 32-bit integer (RGBA format). + * + * @return 32-bit unsigned integer in RGBA format (0xRRGGBBAA) + */ + unsigned int ToRGBA() const { + return (static_cast(r) << 24) | + (static_cast(g) << 16) | + (static_cast(b) << 8) | + static_cast(a); + } + + /** + * @brief Converts the color to a 32-bit integer (ARGB format). + * + * @return 32-bit unsigned integer in ARGB format (0xAARRGGBB) + */ + unsigned int ToARGB() const { + return (static_cast(a) << 24) | + (static_cast(r) << 16) | + (static_cast(g) << 8) | + static_cast(b); + } + + // Common color constants + static const Color Transparent; + static const Color Black; + static const Color White; + static const Color Red; + static const Color Green; + static const Color Blue; + static const Color Yellow; + static const Color Cyan; + static const Color Magenta; +}; + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/foundation/dispatcher.cpp b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher.cpp new file mode 100644 index 0000000..2fc15c3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher.cpp @@ -0,0 +1,59 @@ +#include "dispatcher.h" + +#include "dispatcher_platform.h" + +namespace nativeapi { + +namespace { + +// Embedder/test overrides. Documented as "install before other threads start +// dispatching", so these are plain globals rather than lock-protected state — +// adding a lock here would put a mutex on every event delivery to buy safety +// for a case the contract already excludes. +MainThreadDispatchFn g_dispatch_override; +MainThreadPredicateFn g_predicate_override; + +} // namespace + +bool IsMainThread() { + if (g_predicate_override) { + return g_predicate_override(); + } + return dispatcher_platform::PlatformIsMainThread(); +} + +void SetMainThread() { + dispatcher_platform::PlatformSetMainThread(); +} + +bool IsMainThreadDispatchSupported() { + if (g_dispatch_override) { + return true; + } + return dispatcher_platform::PlatformIsMainThreadDispatchSupported(); +} + +bool RunOnMainThread(std::function fn) { + if (!fn) { + return true; + } + if (g_dispatch_override) { + return g_dispatch_override(std::move(fn)); + } + return dispatcher_platform::PlatformRunOnMainThread(std::move(fn)); +} + +bool RunMainThreadLoopFor(int timeout_ms) { + if (g_dispatch_override) { + // An embedder-supplied scheduler owns its own draining; we have no queue. + return false; + } + return dispatcher_platform::PlatformRunMainThreadLoopFor(timeout_ms); +} + +void SetMainThreadDispatcher(MainThreadDispatchFn dispatch, MainThreadPredicateFn predicate) { + g_dispatch_override = std::move(dispatch); + g_predicate_override = std::move(predicate); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/dispatcher.h b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher.h new file mode 100644 index 0000000..eb600c6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher.h @@ -0,0 +1,125 @@ +#pragma once + +#include + +namespace nativeapi { + +/** + * @file dispatcher.h + * @brief Main/UI thread dispatch primitives. + * + * Every platform this library targets requires UI objects (windows, menus, tray + * icons) to be touched only from the platform's main thread. Before this + * abstraction existed, the rule was stated in doc comments but the library had + * no way to honour it: background threads delivered events straight into user + * callbacks, and each platform file open-coded its own `dispatch_async` / + * `PostMessage` when it happened to remember. + * + * This header is the single place that knowledge now lives. + */ + +/** Function that queues work to run on the main thread. @see SetMainThreadDispatcher. */ +using MainThreadDispatchFn = std::function)>; + +/** Predicate reporting whether the caller is on the main thread. */ +using MainThreadPredicateFn = std::function; + +/** + * @brief Whether the calling thread is the platform's main/UI thread. + * + * On Apple platforms this is answered by the OS. Elsewhere the main thread is + * the one that ran this module's static initializers — i.e. the thread that + * loaded the library, which for a normal application is the thread that later + * enters main(). Call SetMainThread() if that assumption does not hold for your + * embedding (for example, a plugin loaded from a worker thread). + */ +bool IsMainThread(); + +/** + * @brief Declare the calling thread to be the main/UI thread. + * + * Only needed when the automatic detection above is wrong. Must be called + * before any other dispatcher use. Has no effect on Apple platforms, where the + * OS is authoritative. + * + * On Windows this additionally primes the message-only window used for + * dispatch, so calling it once during startup is good practice there. + */ +void SetMainThread(); + +/** + * @brief Whether RunOnMainThread() can actually deliver on this platform. + * + * Returns false where no main-thread dispatch mechanism is wired up yet + * (currently Android and OpenHarmony). Callers that must not silently drop work + * should check this first. + */ +bool IsMainThreadDispatchSupported(); + +/** + * @brief Post @p fn to run on the platform's main/UI thread. + * + * Always asynchronous: the function returns as soon as the work is queued, even + * when called from the main thread itself. That is deliberate — it means the + * ordering guarantees do not depend on which thread the caller happens to be + * on, and it guarantees the callee never runs while the caller still holds a + * lock it took before calling. + * + * Callers that want "run inline if already on the main thread" should compose + * it explicitly: + * + * @code + * if (IsMainThread()) { + * fn(); + * } else { + * RunOnMainThread(std::move(fn)); + * } + * @endcode + * + * @param fn Work to run. Ignored if empty. + * @return true if the work was queued. false if there is no main-thread + * dispatch mechanism, in which case @p fn is NOT run. + */ +bool RunOnMainThread(std::function fn); + +/** + * @brief Route main-thread dispatch through a caller-supplied scheduler. + * + * Two audiences: + * + * - Embedders that already own the main loop (Qt, a game engine, a host + * application with its own task queue) and want library callbacks to arrive + * through the same scheduler rather than a second, parallel mechanism. + * - Tests, which have no run loop at all and need to drain queued work + * deterministically. + * + * Pass @p dispatch as nullptr to restore the platform default. When @p predicate + * is null the platform's own main-thread detection stays in effect. + * + * Must be called before other threads start using the dispatcher; the override + * is not synchronized against concurrent dispatch. + */ +void SetMainThreadDispatcher(MainThreadDispatchFn dispatch, MainThreadPredicateFn predicate); + +/** + * @brief Service main-thread work for up to @p timeout_ms, then return. + * + * Who needs this: console tools, tests, and any embedding that has no UI + * framework of its own. Since RunOnMainThread() hands work to the platform's + * main loop (the GCD main queue, the Win32 message queue, the GLib main + * context), something has to actually run that loop or the work never happens. + * + * Who must NOT call this: applications already running a UI event loop — Cocoa, + * Win32, GTK, Flutter, Qt. Their loop services the same queue, and nesting a + * second one invites re-entrancy bugs. + * + * Must be called from the main thread. + * + * @param timeout_ms How long to service work before returning. 0 drains only + * what is already pending and returns immediately. + * @return false if the platform has no main-loop integration (Android/OHOS), + * in which case nothing was serviced. + */ +bool RunMainThreadLoopFor(int timeout_ms); + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/dispatcher_common.h b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher_common.h new file mode 100644 index 0000000..ebe4d48 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher_common.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace nativeapi { +namespace dispatcher_internal { + +/** + * @brief The thread treated as "main" on platforms where the OS cannot tell us. + * + * Dynamically initialized during static initialization, so it captures the + * thread that loaded the library. For a normal application that is the same + * thread that later enters main(); for an oddly-embedded plugin it may not be, + * which is what SetMainThread() exists to correct. + * + * An inline variable gives exactly one instance across all translation units. + * No synchronization: SetMainThread() is documented as "before any other use", + * and after that point this is read-only. + */ +inline std::thread::id g_main_thread_id = std::this_thread::get_id(); + +inline bool IsMainThreadByCapturedId() { + return std::this_thread::get_id() == g_main_thread_id; +} + +inline void CaptureCallerAsMainThread() { + g_main_thread_id = std::this_thread::get_id(); +} + +} // namespace dispatcher_internal +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/dispatcher_platform.h b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher_platform.h new file mode 100644 index 0000000..046c9e5 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/dispatcher_platform.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace nativeapi { +namespace dispatcher_platform { + +/** + * @file dispatcher_platform.h + * @brief Internal seam between the public dispatcher API and each platform. + * + * Platform files under src/platform//dispatcher_.* implement these. + * The public entry points in dispatcher.cpp own the override logic and forward + * here when no override is installed, so platform code never has to know about + * embedder-supplied schedulers. + */ + +bool PlatformIsMainThread(); +void PlatformSetMainThread(); +bool PlatformIsMainThreadDispatchSupported(); +bool PlatformRunOnMainThread(std::function fn); +bool PlatformRunMainThreadLoopFor(int timeout_ms); + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/event.h b/packages/cnativeapi/cxx_impl/src/foundation/event.h new file mode 100644 index 0000000..13a145c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/event.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include + +namespace nativeapi { + +/** + * Base class for all events in the generic event system. + * Events should inherit from this class and provide their own data. + */ +class Event { + public: + Event() : timestamp_(std::chrono::steady_clock::now()) {} + virtual ~Event() = default; + + // Get the time when this event was created + std::chrono::steady_clock::time_point GetTimestamp() const { return timestamp_; } + + // Get a string representation of the event type (for debugging) + virtual std::string GetTypeName() const = 0; + + private: + std::chrono::steady_clock::time_point timestamp_; +}; + +/** + * Generic event listener interface providing type-safe event handling. + * + * This interface supports both generic and specific event handling: + * - Use EventListener to handle all event types (requires manual type checking) + * - Use EventListener for compile-time type safety with specific events + * + * Example: + * ```cpp + * class MyListener : public EventListener { + * public: + * void OnEvent(const MyCustomEvent& event) override { + * // Handle the event with full type safety + * } + * }; + * ``` + */ +template +class EventListener { + public: + virtual ~EventListener() = default; + + /** + * Handles an incoming event of type T. + * + * The event parameter is guaranteed to be of type T or a subtype. + * Implementation should process the event according to the listener's logic. + */ + virtual void OnEvent(const T& event) = 0; +}; + +/** + * A callback-based event listener that wraps function callbacks into the EventListener interface. + * + * This implementation allows using function references, lambda functions, or any callable + * as event handlers without requiring a full class implementation. It's particularly useful + * for simple event handling scenarios or when you want to use inline functions. + * + * Example usage: + * ```cpp + * // Using a lambda function + * auto listener = std::make_unique>( + * [](const MyEvent& event) { std::cout << "Received: " << event << std::endl; }); + * + * // Using a function reference + * void handleMyEvent(const MyEvent& event) { // handle event } + * auto listener = std::make_unique>(handleMyEvent); + * ``` + */ +template +class CallbackEventListener : public EventListener { + public: + using CallbackType = std::function; + + /** + * Creates a new callback-based event listener with the specified callback function. + * + * The callback must accept a single parameter of type T and return void. + */ + explicit CallbackEventListener(CallbackType callback) : callback_(std::move(callback)) {} + + void OnEvent(const T& event) override { + if (callback_) { + callback_(event); + } + } + + private: + CallbackType callback_; +}; + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/foundation/event_emitter.h b/packages/cnativeapi/cxx_impl/src/foundation/event_emitter.h new file mode 100644 index 0000000..85d8f7e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/event_emitter.h @@ -0,0 +1,592 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dispatcher.h" +#include "event.h" + +namespace nativeapi { + +/** + * Base class that provides event emission capabilities with type constraints. + * Classes that inherit from EventEmitter must specify the base event type they work with, + * and then only emit events of that type or its subclasses. This provides compile-time + * type safety and makes the API more explicit about what events a class can produce. + * + * Template Parameters: + * BaseEventType - The base event type this emitter can handle. All emitted events + * must be of this type or inherit from it. + * + * Threading and re-entrancy guarantees: + * - Listener callbacks are ALWAYS invoked without holding any internal lock. + * A callback may freely call AddListener(), RemoveListener(), or Emit() on the + * same emitter without deadlocking. + * - Emit() is synchronous: listeners run on the thread that called it. Platform + * code is expected to call it from the main thread. + * - EmitAsync() defers to the platform main/UI thread (see dispatcher.h). It is + * the right choice whenever the event originates on a background thread, or + * whenever the caller holds a lock it does not want user code running under. + * - Removing a listener from inside its own callback is safe. The listener object + * stays alive for the duration of the callback, and no further events are + * delivered to it once removed. + * - StartEventListening() / StopEventListening() are invoked without holding the + * listener lock, so platform implementations may call back into the emitter. + * - Listeners are invoked in registration order, regardless of event type. + * + * Dispatch semantics: + * An emitted event is delivered to every listener whose registered type is the + * event's dynamic type or one of its base types (up to BaseEventType). The + * resolution of "which listeners match this event type" is cached per dynamic + * event type, so steady-state dispatch is a single hash lookup rather than a + * scan over every registered listener. + * + * Example usage: + * + * // Define your event hierarchy + * class MyEvent : public Event { + * public: + * std::string data; + * MyEvent(std::string d) : data(std::move(d)) {} + * std::string GetTypeName() const override { return "MyEvent"; } + * }; + * + * class MySpecificEvent : public MyEvent { + * public: + * int value; + * MySpecificEvent(std::string d, int v) : MyEvent(std::move(d)), value(v) {} + * std::string GetTypeName() const override { return "MySpecificEvent"; } + * }; + * + * // Create a class that only emits MyEvent and its subclasses + * class MyClass : public EventEmitter { + * public: + * ~MyClass() override { ShutdownEmitter(); } // see ShutdownEmitter() docs + * + * void DoSomething() { + * // Emit a MyEvent - OK + * Emit("some data"); + * + * // Emit a MySpecificEvent (subclass of MyEvent) - OK + * EmitAsync("data", 42); + * + * // Emit() - Compile error! Not a subclass of MyEvent + * } + * }; + * + * MyClass obj; + * // Listen for base event type + * obj.AddListener([](const MyEvent& event) { + * std::cout << "MyEvent: " << event.data << std::endl; + * }); + * + * // Listen for specific event type + * obj.AddListener([](const MySpecificEvent& event) { + * std::cout << "MySpecificEvent: " << event.data << ", " << event.value << std::endl; + * }); + */ +template +class EventEmitter { + // Ensure BaseEventType is derived from Event + static_assert(std::is_base_of::value, + "BaseEventType must be derived from Event"); + + private: + /** + * Type-erased listener record. + * + * Entries are held by shared_ptr so that a dispatch snapshot keeps the record + * alive even if the listener is removed concurrently (or by its own callback). + * `removed` is the tombstone: once set, no further callbacks are delivered, + * even from a snapshot that was taken before the removal. + */ + struct ListenerEntry { + ListenerEntry(std::type_index type, size_t identifier) + : event_type(type), id(identifier), removed(false) {} + virtual ~ListenerEntry() = default; + + /** Whether this listener accepts an event with the given dynamic type. */ + virtual bool Matches(const BaseEventType& event) const = 0; + + /** Deliver the event. Only called when Matches() returned true. */ + virtual void Invoke(const BaseEventType& event) = 0; + + std::type_index event_type; + size_t id; + std::atomic removed; + }; + + using ListenerEntryPtr = std::shared_ptr; + + public: + // Initializer order follows member declaration order. + EventEmitter() + : listening_active_(false), + dispatch_guard_(std::make_shared()), + next_listener_id_(1) {} + + /** + * Destructor. + * + * Calls ShutdownEmitter(). Note that by the time a base-class destructor runs, + * the derived object has already been destroyed — so derived classes that emit + * events from other threads should call ShutdownEmitter() at the top of their + * own destructor. See ShutdownEmitter(). + */ + virtual ~EventEmitter() { ShutdownEmitter(); } + + /** + * Add a typed event listener for a specific event type. + * The event type must be BaseEventType or a subclass of it. + * + * @param listener Pointer to the event listener (must remain valid until removed) + * @return A unique listener ID that can be used to remove the listener + */ + template + size_t AddListener(EventListener* listener) { + static_assert(std::is_base_of::value, + "EventType must be derived from the EventEmitter's BaseEventType"); + + struct TypedListenerWrapper : public ListenerEntry { + EventListener* listener_; + + TypedListenerWrapper(EventListener* listener, size_t id) + : ListenerEntry(std::type_index(typeid(EventType)), id), listener_(listener) {} + + bool Matches(const BaseEventType& event) const override { + return dynamic_cast(&event) != nullptr; + } + + void Invoke(const BaseEventType& event) override { + if (auto* typed_event = dynamic_cast(&event)) { + listener_->OnEvent(*typed_event); + } + } + }; + + const size_t listener_id = next_listener_id_.fetch_add(1); + return AddListenerEntry(std::make_shared(listener, listener_id)); + } + + /** + * Add a callback function as a listener for a specific event type. + * The event type must be BaseEventType or a subclass of it. + * + * @param callback Function to call when the event occurs + * @return A unique listener ID that can be used to remove the listener + */ + template + size_t AddListener(std::function callback) { + static_assert(std::is_base_of::value, + "EventType must be derived from the EventEmitter's BaseEventType"); + + struct CallbackListenerWrapper : public ListenerEntry { + std::function callback_; + + CallbackListenerWrapper(std::function callback, size_t id) + : ListenerEntry(std::type_index(typeid(EventType)), id), + callback_(std::move(callback)) {} + + bool Matches(const BaseEventType& event) const override { + return dynamic_cast(&event) != nullptr; + } + + void Invoke(const BaseEventType& event) override { + if (auto* typed_event = dynamic_cast(&event)) { + if (callback_) { + callback_(*typed_event); + } + } + } + }; + + const size_t listener_id = next_listener_id_.fetch_add(1); + return AddListenerEntry( + std::make_shared(std::move(callback), listener_id)); + } + + /** + * Remove a listener by its ID. + * + * Safe to call from inside a listener callback, including for the listener + * currently being invoked. + * + * @param listener_id The ID returned by AddListener + * @return true if the listener was found and removed, false otherwise + */ + bool RemoveListener(size_t listener_id) { + bool removed = false; + bool became_empty = false; + + { + std::lock_guard lock(listeners_mutex_); + + auto it = std::find_if( + listeners_.begin(), listeners_.end(), + [listener_id](const ListenerEntryPtr& entry) { return entry->id == listener_id; }); + + if (it != listeners_.end()) { + // Tombstone first: a dispatch snapshot taken before this point must not + // deliver any further events to this listener. + (*it)->removed.store(true); + listeners_.erase(it); + dispatch_cache_.clear(); + removed = true; + became_empty = listeners_.empty(); + } + } + + if (became_empty) { + UpdateListeningState(false); + } + + return removed; + } + + /** + * Remove all listeners for a specific event type. + * The event type must be BaseEventType or a subclass of it. + */ + template + void RemoveAllListeners() { + static_assert(std::is_base_of::value, + "EventType must be derived from the EventEmitter's BaseEventType"); + RemoveAllListeners(std::type_index(typeid(EventType))); + } + + /** + * Remove all listeners for all event types. + */ + void RemoveAllListeners() { + bool had_listeners = false; + + { + std::lock_guard lock(listeners_mutex_); + + had_listeners = !listeners_.empty(); + for (const auto& entry : listeners_) { + entry->removed.store(true); + } + listeners_.clear(); + dispatch_cache_.clear(); + } + + if (had_listeners) { + UpdateListeningState(false); + } + } + + /** + * Get the number of listeners registered for a specific event type. + * The event type must be BaseEventType or a subclass of it. + */ + template + size_t GetListenerCount() const { + static_assert(std::is_base_of::value, + "EventType must be derived from the EventEmitter's BaseEventType"); + return GetListenerCount(std::type_index(typeid(EventType))); + } + + /** + * Get the total number of registered listeners. + */ + size_t GetTotalListenerCount() const { + std::lock_guard lock(listeners_mutex_); + return listeners_.size(); + } + + /** + * Check if there are any listeners for a specific event type. + */ + template + bool HasListeners() const { + return GetListenerCount() > 0; + } + + /** + * Emit an event synchronously to all registered listeners. + * This is a public method for internal use by platform implementations. + * The event must be of BaseEventType or a subclass. + * + * Listener callbacks are invoked without holding any internal lock. + * + * @param event The event to emit + */ + void Emit(const BaseEventType& event) { + std::vector snapshot; + + { + std::lock_guard lock(listeners_mutex_); + snapshot = ResolveListenersLocked(event); + } + + // Dispatch outside the lock so callbacks may re-enter the emitter. + for (const auto& entry : snapshot) { + if (entry->removed.load()) { + continue; // Removed after the snapshot was taken (possibly by an earlier callback). + } + entry->Invoke(event); + } + } + + protected: + /** + * Called when the first listener is added. + * Subclasses can override this to start platform-specific event monitoring. + * + * This is called WITHOUT holding any internal lock, so implementations may + * safely call back into the emitter. + */ + virtual void StartEventListening() {} + + /** + * Called when the last listener is removed. + * Subclasses can override this to stop platform-specific event monitoring. + * + * This is called WITHOUT holding any internal lock, so implementations may + * safely call back into the emitter. + */ + virtual void StopEventListening() {} + + /** + * Detach from pending async dispatch and drop all listeners. + * + * Derived classes that emit events asynchronously should call this at the TOP + * of their own destructor. By the time ~EventEmitter() runs, the derived + * portion of the object is already destroyed, so a queued EmitAsync() landing + * on the main thread would otherwise dispatch into a half-destroyed object. + * + * Blocks until any dispatch already inside the guard has finished, then marks + * the emitter dead so later arrivals become no-ops. + * + * Idempotent — safe to call multiple times. + */ + void ShutdownEmitter() { + { + std::lock_guard lock(dispatch_guard_->mutex); + dispatch_guard_->alive = false; + } + RemoveAllListeners(); + } + + /** + * Emit an event synchronously using perfect forwarding. + * This creates the event object and emits it immediately. + * The event type must be BaseEventType or a subclass of it. + */ + template + void Emit(Args&&... args) { + static_assert(std::is_base_of::value, + "EventType must be derived from the EventEmitter's BaseEventType"); + + EventType event(std::forward(args)...); + Emit(event); + } + + /** + * Emit an event asynchronously, on the platform's main/UI thread. + * + * Delivery is always deferred, even when called from the main thread. That + * matters for the library's own callers: ShortcutManager emits while holding + * its internal mutex, and deferring is what keeps user callbacks from running + * underneath a lock they know nothing about. + * + * The event must be of BaseEventType or a subclass. + * + * @param event The event to emit (will be moved) + */ + void EmitAsync(std::unique_ptr event) { + if (!event) { + return; + } + + std::shared_ptr shared_event(std::move(event)); + auto guard = dispatch_guard_; + EventEmitter* self = this; + + const bool queued = RunOnMainThread([guard, self, shared_event]() { + std::lock_guard lock(guard->mutex); + if (!guard->alive) { + return; // Emitter was destroyed after this work was queued. + } + self->Emit(*shared_event); + }); + + if (!queued) { + // No main-thread dispatch on this platform (currently Android/OpenHarmony). + // + // Deliver inline rather than dropping the event: losing notifications + // silently is a worse failure than delivering them on the calling thread, + // and the calling thread is what these platforms did before anyway. + // + // Caveat: this reintroduces "callback runs under the emitter's caller's + // lock" on those platforms. IsMainThreadDispatchSupported() reports the + // situation; wiring the remaining loopers is tracked as TODO in the + // respective dispatcher_*.cpp. + Emit(*shared_event); + } + } + + /** + * Emit an event asynchronously using perfect forwarding. + * The event type must be BaseEventType or a subclass of it. + */ + template + void EmitAsync(Args&&... args) { + static_assert(std::is_base_of::value, + "EventType must be derived from the EventEmitter's BaseEventType"); + + auto event = std::make_unique(std::forward(args)...); + EmitAsync(std::move(event)); + } + + private: + size_t AddListenerEntry(ListenerEntryPtr entry) { + const size_t listener_id = entry->id; + bool was_empty = false; + + { + std::lock_guard lock(listeners_mutex_); + was_empty = listeners_.empty(); + listeners_.push_back(std::move(entry)); + dispatch_cache_.clear(); + } + + if (was_empty) { + UpdateListeningState(true); + } + + return listener_id; + } + + void RemoveAllListeners(std::type_index event_type) { + bool became_empty = false; + bool removed_any = false; + + { + std::lock_guard lock(listeners_mutex_); + + const size_t before = listeners_.size(); + auto new_end = std::remove_if(listeners_.begin(), listeners_.end(), + [event_type](const ListenerEntryPtr& entry) { + if (entry->event_type == event_type) { + entry->removed.store(true); + return true; + } + return false; + }); + listeners_.erase(new_end, listeners_.end()); + + removed_any = listeners_.size() != before; + if (removed_any) { + dispatch_cache_.clear(); + } + became_empty = removed_any && listeners_.empty(); + } + + if (became_empty) { + UpdateListeningState(false); + } + } + + size_t GetListenerCount(std::type_index event_type) const { + std::lock_guard lock(listeners_mutex_); + return static_cast( + std::count_if(listeners_.begin(), listeners_.end(), + [event_type](const ListenerEntryPtr& entry) { + return entry->event_type == event_type; + })); + } + + /** + * Serializes StartEventListening() / StopEventListening() transitions. + * + * Must be called WITHOUT holding listeners_mutex_. The dedicated mutex keeps + * the start/stop pair ordered even when concurrent add/remove operations race. + * + * The mutex is recursive on purpose: a platform hook is allowed to add or + * remove listeners from inside StartEventListening()/StopEventListening(), + * which re-enters this function on the same thread. A plain mutex would + * deadlock there, which is exactly the class of bug this rewrite removes. + */ + void UpdateListeningState(bool should_listen) { + std::lock_guard lock(listening_mutex_); + + if (should_listen == listening_active_) { + return; + } + listening_active_ = should_listen; + + if (should_listen) { + StartEventListening(); + } else { + StopEventListening(); + } + } + + /** + * Resolve the listeners that should receive this event. + * + * Caller must hold listeners_mutex_. The per-dynamic-type resolution is cached, + * so the dynamic_cast scan over all listeners only happens on a cache miss. + */ + std::vector ResolveListenersLocked(const BaseEventType& event) { + const std::type_index dynamic_type(typeid(event)); + + auto cached = dispatch_cache_.find(dynamic_type); + if (cached != dispatch_cache_.end()) { + return cached->second; + } + + std::vector matched; + for (const auto& entry : listeners_) { + if (entry->Matches(event)) { + matched.push_back(entry); + } + } + + dispatch_cache_.emplace(dynamic_type, matched); + return matched; + } + + /** + * Keeps queued async dispatch from touching a destroyed emitter. + * + * Held by shared_ptr so a posted lambda can outlive the emitter and still have + * something valid to inspect. Recursive because a listener callback is allowed + * to destroy the emitter, which re-enters this mutex on the same thread. + */ + struct DispatchGuard { + std::recursive_mutex mutex; + bool alive = true; + }; + + // Listener registry. Insertion-ordered so dispatch order is deterministic. + mutable std::mutex listeners_mutex_; + std::vector listeners_; + + // Cache: dynamic event type -> listeners that accept it. + // Invalidated wholesale whenever the listener set changes. + std::unordered_map> dispatch_cache_; + + // Serializes StartEventListening()/StopEventListening() transitions. + // Recursive: platform hooks may add/remove listeners re-entrantly. + std::recursive_mutex listening_mutex_; + bool listening_active_; + + // Shared with in-flight async dispatch; see DispatchGuard. + std::shared_ptr dispatch_guard_; + + // Listener ID generation + std::atomic next_listener_id_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/geometry.h b/packages/cnativeapi/cxx_impl/src/foundation/geometry.h new file mode 100644 index 0000000..84578ce --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/geometry.h @@ -0,0 +1,31 @@ +#pragma once + +namespace nativeapi { + +/** + * Point is a 2D point in the coordinate system. + */ +struct Point { + double x; + double y; +}; + +/** + * Size is a 2D size in the coordinate system. + */ +struct Size { + double width; + double height; +}; + +/** + * Rectangle is a 2D rectangle in the coordinate system. + */ +struct Rectangle { + double x; + double y; + double width; + double height; +}; + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/foundation/handle_table.cpp b/packages/cnativeapi/cxx_impl/src/foundation/handle_table.cpp new file mode 100644 index 0000000..6c5c162 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/handle_table.cpp @@ -0,0 +1,114 @@ +#include "handle_table.h" + +namespace nativeapi { + +HandleTable& HandleTable::GetInstance() { + static HandleTable instance; + return instance; +} + +HandleValue HandleTable::InsertErased(std::shared_ptr object, uint32_t type_tag) { + std::lock_guard lock(mutex_); + + uint32_t slot_index; + if (!free_slots_.empty()) { + slot_index = free_slots_.back(); + free_slots_.pop_back(); + } else { + slot_index = static_cast(slots_.size()); + slots_.emplace_back(); + } + + Slot& slot = slots_[slot_index]; + slot.type_tag = type_tag; + slot.object = std::move(object); + + return Encode(slot.generation, slot_index); +} + +const HandleTable::Slot* HandleTable::FindLiveSlotLocked(HandleValue handle) const { + if (handle == kInvalidHandle) { + return nullptr; + } + + const uint32_t slot_index = SlotOf(handle); + if (slot_index >= slots_.size()) { + return nullptr; + } + + const Slot& slot = slots_[slot_index]; + if (!slot.object) { + return nullptr; // Slot was released. + } + if (slot.generation != GenerationOf(handle)) { + return nullptr; // Stale handle to a slot that has since been reused. + } + return &slot; +} + +std::shared_ptr HandleTable::ResolveErased(HandleValue handle, uint32_t type_tag) const { + std::lock_guard lock(mutex_); + + const Slot* slot = FindLiveSlotLocked(handle); + if (!slot) { + return nullptr; + } + if (slot->type_tag != type_tag) { + return nullptr; // Handle confusion: right slot, wrong type. + } + + // Returning a copy, not a reference: the caller's strong reference must + // outlive any concurrent Release(). + return slot->object; +} + +bool HandleTable::Release(HandleValue handle) { + // Deliberately deferred past the lock: dropping the last reference runs the + // object's destructor, which may call back into the table (a Window releasing + // child handles, say). Doing that under mutex_ would self-deadlock. + std::shared_ptr doomed; + + { + std::lock_guard lock(mutex_); + + if (!FindLiveSlotLocked(handle)) { + return false; + } + + const uint32_t slot_index = SlotOf(handle); + Slot& slot = slots_[slot_index]; + + doomed = std::move(slot.object); + slot.object = nullptr; + slot.type_tag = 0; + + // Invalidate every outstanding handle to this slot. Skip 0 on wraparound so + // Encode(generation, 0) can never equal kInvalidHandle. + ++slot.generation; + if (slot.generation == 0) { + slot.generation = 1; + } + + free_slots_.push_back(slot_index); + } + + return true; +} + +bool HandleTable::Contains(HandleValue handle) const { + std::lock_guard lock(mutex_); + return FindLiveSlotLocked(handle) != nullptr; +} + +uint32_t HandleTable::GetTypeTag(HandleValue handle) const { + std::lock_guard lock(mutex_); + const Slot* slot = FindLiveSlotLocked(handle); + return slot ? slot->type_tag : 0u; +} + +size_t HandleTable::LiveCount() const { + std::lock_guard lock(mutex_); + return slots_.size() - free_slots_.size(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/handle_table.h b/packages/cnativeapi/cxx_impl/src/foundation/handle_table.h new file mode 100644 index 0000000..12a8f72 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/handle_table.h @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include +#include + +#include "id_allocator.h" + +namespace nativeapi { + +/** + * @file handle_table.h + * @brief Generational handle table backing every C ABI object handle. + * + * The ownership rules this implements are specified in the libnativeapi + * workspace repo (specs/handle-ownership.md). + * + * A handle is an opaque 64-bit integer, never a pointer: + * + * [ generation : 32 | slot index : 32 ] + * + * Resolving one checks three things — that the slot exists, that its generation + * still matches, and that its type tag is the type the caller expects. Releasing + * a handle clears the slot and bumps its generation, which invalidates every + * outstanding handle to that slot at once. + * + * Why this shape: + * + * - Handles cross into Dart and Swift, where finalizers run at unpredictable + * times. With raw pointers, use-after-free and double-free are a matter of + * when, not if. Here a stale handle fails a comparison instead of + * dereferencing freed memory. + * - The type tag turns handle confusion (passing a menu handle to a window + * function) from undefined behaviour into a clean error. + * - The table owns a shared_ptr, so Resolve() can hand back a strong reference + * that keeps the object alive for the duration of the call. That is what + * finally lets std::shared_ptr-based APIs cross the C ABI at all — the single + * biggest blocker on codegen coverage. + */ + +using HandleValue = uint64_t; + +/// Never refers to a live object. Slot generations start at 1, so a zeroed +/// handle can never collide with a real one. +constexpr HandleValue kInvalidHandle = 0; + +class HandleTable { + public: + /** + * @brief Process-wide table. + * + * A singleton for now, matching how the C ABI is structured. It is a Meyer's + * singleton like the rest of the library; the explicit-lifetime work in + * DESIGN_REVIEW §4.2 will fold this into the same Context as the others. + */ + static HandleTable& GetInstance(); + + /** + * @brief Store a strong reference and return a fresh handle for it. + * + * @return kInvalidHandle if @p object is null. + */ + template + HandleValue Insert(std::shared_ptr object) { + if (!object) { + return kInvalidHandle; + } + return InsertErased(std::static_pointer_cast(std::move(object)), + IdTypeTag::value); + } + + /** + * @brief Look up a handle, returning a strong reference. + * + * The returned shared_ptr keeps the object alive even if another thread + * releases the handle concurrently — which is precisely the guarantee the old + * raw-pointer handles could not make. + * + * @return nullptr if the handle is stale, unknown, or refers to a different + * type than @p T. + */ + template + std::shared_ptr Resolve(HandleValue handle) const { + auto erased = ResolveErased(handle, IdTypeTag::value); + if (!erased) { + return nullptr; + } + // Safe: the type tag check above proves this slot was filled by + // Insert(), so the stored pointer really is a T*. + return std::static_pointer_cast(std::move(erased)); + } + + /** + * @brief Drop the table's reference and invalidate the handle. + * + * Idempotent by construction: releasing an already-released or bogus handle + * returns false and does nothing. The object itself is destroyed only when the + * last strong reference goes away, which may be later if a Resolve() result is + * still in scope somewhere. + * + * @return true if this call released a live handle. + */ + bool Release(HandleValue handle); + + /** @brief Whether @p handle currently resolves, ignoring type. */ + bool Contains(HandleValue handle) const; + + /** @brief Type tag stored for @p handle, or 0 if it does not resolve. */ + uint32_t GetTypeTag(HandleValue handle) const; + + /** @brief Number of live handles. Intended for tests and leak checks. */ + size_t LiveCount() const; + + // Handle encoding helpers, exposed for tests and diagnostics. + static constexpr uint32_t SlotOf(HandleValue handle) { + return static_cast(handle & 0xFFFFFFFFull); + } + static constexpr uint32_t GenerationOf(HandleValue handle) { + return static_cast((handle >> 32) & 0xFFFFFFFFull); + } + static constexpr HandleValue Encode(uint32_t generation, uint32_t slot) { + return (static_cast(generation) << 32) | static_cast(slot); + } + + private: + HandleTable() = default; + HandleTable(const HandleTable&) = delete; + HandleTable& operator=(const HandleTable&) = delete; + + struct Slot { + /// Odd/even is not used; a slot is live iff `object` is non-null. + /// Starts at 1 so that Encode(0, 0) == kInvalidHandle is unreachable. + uint32_t generation = 1; + uint32_t type_tag = 0; + std::shared_ptr object; + }; + + HandleValue InsertErased(std::shared_ptr object, uint32_t type_tag); + std::shared_ptr ResolveErased(HandleValue handle, uint32_t type_tag) const; + + /// Caller must hold mutex_. Returns nullptr if the handle does not resolve. + const Slot* FindLiveSlotLocked(HandleValue handle) const; + + mutable std::mutex mutex_; + std::vector slots_; + std::vector free_slots_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/id_allocator.cpp b/packages/cnativeapi/cxx_impl/src/foundation/id_allocator.cpp new file mode 100644 index 0000000..694c86c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/id_allocator.cpp @@ -0,0 +1,58 @@ +/** + * Implementation of IdAllocator static methods for ID querying and validation. + */ + +#include "id_allocator.h" + +namespace nativeapi { + +/** + * Extracts the type from an ID. + */ +uint32_t IdAllocator::GetType(IdType id) { + // Extract type value from high 8 bits (bits 31-24) + // kTypeMask = 0xFF000000, kTypeShift = 24 + const uint32_t type_value = (id & kTypeMask) >> kTypeShift; + return type_value; +} + +/** + * Extracts the sequence number from an ID. + */ +uint32_t IdAllocator::GetSequence(IdType id) { + // Extract sequence number from low 24 bits (bits 23-0) + // kSequenceMask = 0x00FFFFFF + return id & kSequenceMask; +} + +/** + * Checks if an ID is valid. + */ +bool IdAllocator::IsValid(IdType id) { + // Extract type value from high 8 bits + const uint32_t type_value = (id & kTypeMask) >> kTypeShift; + + // Extract sequence number from low 24 bits + const uint32_t seq = id & kSequenceMask; + + // ID is valid if: + // 1. Type value is in valid range [kMinTypeValue, kMaxTypeValue] (1-10) + // 2. Sequence number is non-zero (0 is reserved for kInvalidId) + return IsValidType(type_value) && seq != 0u; +} + +/** + * Extracts both type and sequence from an ID. + */ +std::pair IdAllocator::Decompose(IdType id) { + // Extract type value from high 8 bits (bits 31-24) + const uint32_t type_value = (id & kTypeMask) >> kTypeShift; + + // Extract sequence number from low 24 bits (bits 23-0) + const uint32_t sequence = id & kSequenceMask; + + // Return both components as a pair + return {type_value, sequence}; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/id_allocator.h b/packages/cnativeapi/cxx_impl/src/foundation/id_allocator.h new file mode 100644 index 0000000..62b50a1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/id_allocator.h @@ -0,0 +1,280 @@ +#pragma once + +#include +#include +#include + +namespace nativeapi { + +/** + * @brief Compile-time type tag baked into every allocated ID. + * + * Deliberately left undefined in the primary template: a type must opt in by + * specializing this, and calling IdAllocator::Allocate() for an unregistered + * T is a compile error rather than a silent runtime surprise. + * + * These values are stable identifiers, not arbitrary numbers. They appear in + * the high bits of every ID handed out, and the handle-table work described in + * DESIGN_REVIEW §P0-4 will use them to reject type-confused handles crossing + * the C ABI. Therefore: + * + * - NEVER renumber an existing entry. + * - Only append new entries. + * - Valid range is [kMinTypeValue, kMaxTypeValue]. + * + * (Before this existed, tags were handed out by a runtime counter on a + * first-call-wins basis, so the same C++ type could get a different tag from + * one run to the next — useless for validating anything.) + */ +template +struct IdTypeTag; + +// Forward declarations for the registry below; each type's real definition +// lives in its own header. +class Display; +class Image; +class KeyboardMonitor; +class LaunchAtLogin; +class Menu; +class MenuItem; +class MessageDialog; +class PositioningStrategy; +class Preferences; +class SecureStorage; +class Shortcut; +class TrayIcon; +class Window; + +// --------------------------------------------------------------------------- +// Type tag registry — append only. +// --------------------------------------------------------------------------- +template <> +struct IdTypeTag { + static constexpr uint32_t value = 1; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 2; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 3; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 4; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 5; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 6; +}; +// Tags 7+ exist for the handle table rather than for IdAllocator: these types +// never allocate an ID of their own, but every type that crosses the C ABI as a +// handle needs a tag so the table can reject type-confused handles. +template <> +struct IdTypeTag { + static constexpr uint32_t value = 7; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 8; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 9; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 10; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 11; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 12; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 13; +}; + +/** + * Thread-safe ID allocator with type information. + * + * Each ID is a 32-bit value: [Type:8 bits][Sequence:24 bits] + * Provides unique IDs for different object types with thread-safe allocation. + * + * ID Structure (32 bits): + * +------------+--------------------------+ + * | Type (8) | Sequence (24) | + * +------------+--------------------------+ + * Bits: 31-24 23-0 + * + * Field Details: + * - Type: 8-bit type identifier (1-255, 0 reserved for invalid), taken from the + * compile-time IdTypeTag registry above — stable across runs. + * - Sequence: 24-bit sequence number (1-16777215, 0 reserved for invalid) + * - Invalid ID: 0x00000000 (both type and sequence are 0) + * + * Example: + * - Type 1, Sequence 1: 0x01000001 + * - Type 2, Sequence 100: 0x02000064 + * - Type 5, Sequence 1000: 0x050003E8 + * + * Thread Safety: + * - All allocation operations are thread-safe using atomic operations + * - Each type has its own independent sequence counter + * - Type values are compile-time constants, so there is no assignment to race on + */ +class IdAllocator { + public: + using IdType = uint32_t; + static_assert(sizeof(IdType) == 4, "IdAllocator::IdType must be 32-bit"); + + /// Invalid ID value returned on allocation failure + static constexpr IdType kInvalidId = 0u; + + /// Bit layout specification: [ type:8 | sequence:24 ] + /// High 8 bits store the type identifier, low 24 bits store the sequence + /// number + static constexpr uint32_t kTypeBits = 8; ///< Number of bits allocated for type information + static constexpr uint32_t kSequenceBits = 24; ///< Number of bits allocated for sequence numbers + static constexpr uint32_t kTypeShift = 24; ///< Bit shift amount to extract type from ID + static constexpr uint32_t kTypeMask = + 0xFF000000u; ///< Bit mask to extract type bits (high 8 bits) + static constexpr uint32_t kSequenceMask = + 0x00FFFFFFu; ///< Bit mask to extract sequence bits (low 24 bits) + + /// Valid type value range [1, 255] — the full width of the 8-bit type field. + /// Type value 0 is reserved for invalid IDs (kInvalidId). + /// + /// This used to be capped at 10 for no structural reason, and exceeding it + /// failed silently by returning kInvalidId. The field always had room for + /// 255; the cap is simply gone now. Widening IdType itself was considered and + /// rejected — it would ripple into native_*_id_t across the C ABI and all + /// three language bindings to buy headroom nothing is close to needing. + static constexpr uint32_t kMinTypeValue = 1u; ///< Minimum valid type value + static constexpr uint32_t kMaxTypeValue = 255u; ///< Maximum valid type value + + /// Maximum number of unique IDs per type (2^24 - 1 = 16,777,215) + /// Sequence 0 is reserved for invalid IDs, so maximum is kSequenceMask + static constexpr uint32_t kMaxIdsPerType = kSequenceMask; + + private: + /** + * Gets the sequence counter for template type T. + */ + template + static std::atomic& GetCounter() { + static std::atomic counter{0}; + return counter; + } + + /** + * Gets the stable type value for template type T. + * + * Resolved entirely at compile time from the IdTypeTag registry, so the + * same type always yields the same value — across threads, across call + * orders, and across runs. + */ + template + static constexpr uint32_t GetTypeValue() { + static_assert(IsValidType(IdTypeTag::value), + "IdTypeTag::value is outside [kMinTypeValue, kMaxTypeValue]. " + "Register the type in the tag registry in id_allocator.h."); + return IdTypeTag::value; + } + + public: + /** + * Allocates a new unique ID for type T. + * @return A unique ID, or kInvalidId if allocation failed. + */ + template + static IdType Allocate() { + // Stable, compile-time type value from the IdTypeTag registry. + // An unregistered type fails to compile rather than returning kInvalidId. + constexpr uint32_t type_value = GetTypeValue(); + + // Atomically increment the sequence counter for this type and skip 0. + // Using relaxed memory ordering is safe here because we only need + // atomicity, not ordering guarantees between different operations. This + // provides optimal performance while maintaining thread safety. + uint32_t sequence = GetCounter().fetch_add(1, std::memory_order_relaxed) + 1u; + + // Check for overflow: if sequence wraps around to 0 in the low 24 bits, + // treat as allocation failure to avoid returning kInvalidId + if ((sequence & kSequenceMask) == 0u) { + // Sequence counter overflowed - this happens after 2^24 allocations + // Return kInvalidId to indicate allocation failure + return kInvalidId; + } + + // Encode the ID: high 8 bits = type, low 24 bits = sequence + // This creates a unique ID that encodes both type and sequence information + return (type_value << kTypeShift) | (sequence & kSequenceMask); + } + + /** + * Attempts to allocate an ID, returning kInvalidId on failure. + */ + template + static IdType TryAllocate() { + return Allocate(); + } + + // ID Query Methods + + /** + * Extracts the type from an ID. + */ + static uint32_t GetType(IdType id); + + /** + * Extracts the sequence number from an ID. + */ + static uint32_t GetSequence(IdType id); + + /** + * Checks if an ID is valid. + */ + static bool IsValid(IdType id); + + /** + * Extracts both type and sequence from an ID. + */ + static std::pair Decompose(IdType id); + + // Counter Management + + /** + * Gets the current sequence counter for type T. + */ + template + static uint32_t GetCurrentCount() { + return GetCounter().load(std::memory_order_relaxed); + } + + /** + * Resets the sequence counter for type T. + */ + template + static void Reset() { + GetCounter().store(0, std::memory_order_relaxed); + } + + /** + * Validates if a type value is within the valid range. + */ + static constexpr bool IsValidType(uint32_t type_value) { + return type_value >= kMinTypeValue && type_value <= kMaxTypeValue; + } +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/keyboard.cpp b/packages/cnativeapi/cxx_impl/src/foundation/keyboard.cpp new file mode 100644 index 0000000..2b65f04 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/keyboard.cpp @@ -0,0 +1,39 @@ +#include "keyboard.h" +#include + +namespace nativeapi { + +std::string KeyboardAccelerator::ToString() const { + if (key.empty()) { + return ""; + } + + std::ostringstream oss; + + // Add modifiers in a consistent order + if ((modifiers & ModifierKey::Ctrl) != ModifierKey::None) { + oss << "Ctrl+"; + } + if ((modifiers & ModifierKey::Alt) != ModifierKey::None) { + oss << "Alt+"; + } + if ((modifiers & ModifierKey::Shift) != ModifierKey::None) { + oss << "Shift+"; + } + if ((modifiers & ModifierKey::Meta) != ModifierKey::None) { +#ifdef __APPLE__ + oss << "Cmd+"; +#elif defined(_WIN32) + oss << "Win+"; +#else + oss << "Super+"; +#endif + } + + // Add the main key + oss << key; + + return oss.str(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/keyboard.h b/packages/cnativeapi/cxx_impl/src/foundation/keyboard.h new file mode 100644 index 0000000..3b97845 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/keyboard.h @@ -0,0 +1,296 @@ +#pragma once + +#include +#include +#include "event.h" + +namespace nativeapi { + +/** + * @brief Enumeration of keyboard modifier keys. + * + * Defines the various modifier keys that can be combined with regular keys + * to create keyboard shortcuts and accelerators. Modifiers can be combined + * using bitwise OR operations. + */ +enum class ModifierKey : uint32_t { + /** + * No modifier keys pressed. + */ + None = 0, + + /** + * Shift key modifier. + */ + Shift = 1 << 0, + + /** + * Control key modifier (Ctrl on Windows/Linux). + */ + Ctrl = 1 << 1, + + /** + * Alt key modifier (Option on macOS). + */ + Alt = 1 << 2, + + /** + * Meta key modifier (Windows key on Windows, Command key on macOS, Super on Linux). + */ + Meta = 1 << 3, + + /** + * Function key modifier (Fn key, typically on laptops). + */ + Fn = 1 << 4, + + /** + * Caps Lock state indicator. + */ + CapsLock = 1 << 5, + + /** + * Num Lock state indicator. + */ + NumLock = 1 << 6, + + /** + * Scroll Lock state indicator. + */ + ScrollLock = 1 << 7 +}; + +/** + * @brief Bitwise OR operator for combining ModifierKey values. + * + * Allows combining multiple modifier keys using the | operator. + * + * @param a First modifier key + * @param b Second modifier key + * @return Combined modifier keys + * + * @example + * ```cpp + * auto modifiers = ModifierKey::Ctrl | ModifierKey::Shift; + * ``` + */ +inline ModifierKey operator|(ModifierKey a, ModifierKey b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +/** + * @brief Bitwise AND operator for checking ModifierKey values. + * + * Allows checking if specific modifier keys are present using the & operator. + * + * @param a First modifier key + * @param b Second modifier key + * @return Intersection of modifier keys + * + * @example + * ```cpp + * if ((modifiers & ModifierKey::Ctrl) != ModifierKey::None) { + * // Ctrl is pressed + * } + * ``` + */ +inline ModifierKey operator&(ModifierKey a, ModifierKey b) { + return static_cast(static_cast(a) & static_cast(b)); +} + +/** + * @brief Bitwise OR assignment operator for ModifierKey values. + * + * Allows accumulating modifier keys using the |= operator. + * + * @param a Modifier key to modify + * @param b Modifier key to add + * @return Reference to the modified modifier key + * + * @example + * ```cpp + * ModifierKey modifiers = ModifierKey::Ctrl; + * modifiers |= ModifierKey::Shift; + * ``` + */ +inline ModifierKey& operator|=(ModifierKey& a, ModifierKey b) { + a = a | b; + return a; +} + +/** + * @brief Keyboard accelerator for menu items and shortcuts. + * + * Represents a keyboard shortcut that can trigger a menu item or action. + * Supports modifier keys and regular keys in a platform-independent way. + */ +struct KeyboardAccelerator { + /** + * Combination of modifier flags. + */ + ModifierKey modifiers = ModifierKey::None; + + /** + * The main key code (e.g., 'A', 'F1', etc.). + */ + std::string key; + + /** + * Constructor for creating keyboard accelerators. + * + * @param key The main key (e.g., "A", "F1", "Enter") + * @param modifiers Combination of modifier flags + * + * @example + * ```cpp + * // Ctrl+S + * KeyboardAccelerator save_accel("S", ModifierKey::Ctrl); + * + * // Ctrl+Shift+N + * KeyboardAccelerator new_accel("N", + * ModifierKey::Ctrl | ModifierKey::Shift); + * ``` + */ + KeyboardAccelerator(const std::string& key = "", ModifierKey modifiers = ModifierKey::None) + : key(key), modifiers(modifiers) {} + + /** + * Get a human-readable string representation of the accelerator. + * + * @return String representation like "Ctrl+S" or "Alt+F4" + */ + std::string ToString() const; + + /** + * Check if this accelerator is empty (no key specified). + * + * @return true if no key is specified, false otherwise + */ + bool IsEmpty() const { return key.empty(); } + + /** + * Equality comparison operator. + * + * @param other The other accelerator to compare with + * @return true if both accelerators are equal, false otherwise + */ + bool operator==(const KeyboardAccelerator& other) const { + return modifiers == other.modifiers && key == other.key; + } + + /** + * Inequality comparison operator. + * + * @param other The other accelerator to compare with + * @return true if accelerators are different, false otherwise + */ + bool operator!=(const KeyboardAccelerator& other) const { return !(*this == other); } +}; + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * Base class for all keyboard-related events + * + * This class provides common functionality for keyboard events, + * including access to the keycode that triggered the event. + */ +class KeyboardEvent : public Event { + public: + /** + * Constructor for KeyboardEvent + * @param keycode The keycode associated with this event + */ + explicit KeyboardEvent(int keycode) : keycode_(keycode) {} + + /** + * Virtual destructor + */ + virtual ~KeyboardEvent() = default; + + /** + * Get the keycode associated with this event + * @return The keycode value + */ + int GetKeycode() const { return keycode_; } + + /** + * Get a string representation of the event type (for debugging) + * Default implementation returns "KeyboardEvent" + */ + std::string GetTypeName() const override { return "KeyboardEvent"; } + + private: + int keycode_; +}; + +/** + * Event class for key press + * + * This event is emitted when a key is pressed down. + */ +class KeyPressedEvent : public KeyboardEvent { + public: + explicit KeyPressedEvent(int keycode) : KeyboardEvent(keycode) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "KeyPressedEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for key release + * + * This event is emitted when a key is released. + */ +class KeyReleasedEvent : public KeyboardEvent { + public: + explicit KeyReleasedEvent(int keycode) : KeyboardEvent(keycode) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "KeyReleasedEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for modifier keys change + * + * This event is emitted when modifier keys (Ctrl, Alt, Shift, etc.) change state. + */ +class ModifierKeysChangedEvent : public KeyboardEvent { + public: + explicit ModifierKeysChangedEvent(uint32_t modifier_keys) + : KeyboardEvent(0), modifier_keys_(modifier_keys) {} + + /** + * Get the modifier keys state + * @return The modifier keys bitmask + */ + uint32_t GetModifierKeys() const { return modifier_keys_; } + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "ModifierKeysChangedEvent"; } + + /** + * Get the static type index for this event type + */ + + private: + uint32_t modifier_keys_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/foundation/native_object_provider.h b/packages/cnativeapi/cxx_impl/src/foundation/native_object_provider.h new file mode 100644 index 0000000..a50c0e5 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/native_object_provider.h @@ -0,0 +1,78 @@ +#pragma once + +namespace nativeapi { + +/** + * @brief Base class that provides access to platform-specific native objects. + * + * This class provides a standardized way for cross-platform wrapper classes + * to expose their underlying platform-specific objects. Similar to how + * EventEmitter provides event-related functionality, NativeObjectProvider + * provides native object access functionality. + * + * Classes that inherit from NativeObjectProvider can provide access to: + * - NSWindow*, NSMenu*, NSMenuItem* on macOS + * - HWND, HMENU on Windows + * - GtkWidget*, GtkMenu* on Linux + * + * Example usage: + * + * class MyWidget : public NativeObjectProvider { + * public: + * MyWidget() : native_widget_(CreatePlatformWidget()) {} + * + * protected: + * void* GetNativeObjectInternal() const override { + * return native_widget_; + * } + * + * private: + * void* native_widget_; + * }; + * + * // Usage + * MyWidget widget; + * void* native = widget.GetNativeObject(); + * + * #ifdef __APPLE__ + * NSView* nsview = (__bridge NSView*)native; + * #elif defined(_WIN32) + * HWND hwnd = (HWND)native; + * #endif + */ +class NativeObjectProvider { + public: + /** + * @brief Virtual destructor to ensure proper cleanup in derived classes. + */ + virtual ~NativeObjectProvider() = default; + + /** + * @brief Get the native platform-specific object. + * + * This method provides access to the underlying platform-specific + * object for advanced use cases. Use with caution as this breaks + * the abstraction layer. + * + * @return Pointer to the native object + * + * Platform-specific return types: + * - macOS: NSWindow*, NSMenu*, NSMenuItem*, NSView*, etc. + * - Windows: HWND, HMENU, etc. + * - Linux: GtkWidget*, GtkMenu*, GdkWindow*, etc. + */ + void* GetNativeObject() const { return GetNativeObjectInternal(); } + + protected: + /** + * @brief Internal method to be implemented by derived classes. + * + * Derived classes must implement this method to return their + * platform-specific native object. + * + * @return Pointer to the platform-specific native object + */ + virtual void* GetNativeObjectInternal() const = 0; +}; + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/foundation/object_registry.h b/packages/cnativeapi/cxx_impl/src/foundation/object_registry.h new file mode 100644 index 0000000..426faf7 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/foundation/object_registry.h @@ -0,0 +1,126 @@ +/** + * @file object_registry.h + * @brief Thread-safe registry for mapping IDs to shared objects. + * + * This utility provides a minimal, lock-guarded container that stores + * `std::shared_ptr` instances keyed by `TId`. It is intended for + * simple, centralized tracking of live objects (e.g., windows, menus) and + * supports lookup, enumeration, removal, and clearing operations. + * + * Thread-safety: + * - All public methods take a mutex to protect internal state. + * - Methods marked `const` still acquire the mutex (via a `mutable` mutex) + * to ensure safe concurrent access while preserving the const interface. + * + * Requirements: + * - `TId` must be hashable and equality comparable (usable as an + * `unordered_map` key). + * - Objects are stored as `std::shared_ptr`. The registry does not + * impose ownership semantics beyond holding shared references. + */ +#pragma once +#include +#include +#include +#include + +namespace nativeapi { + +template +class ObjectRegistry { + public: + /** + * @brief Add or replace an object for the given ID. + * + * If an entry for `id` already exists, it is replaced by `object`. + * This operation is O(1) average-case. + * + * @param id The identifier used as key in the registry. + * @param object The object to store; moved into the registry. + */ + void Add(TId id, std::shared_ptr object) { + std::lock_guard lock(mutex_); + objects_[id] = std::move(object); + } + + /** + * @brief Get the object associated with an ID. + * + * This operation is O(1) average-case. + * + * @param id The identifier to look up. + * @return std::shared_ptr The stored object if present, + * otherwise `nullptr`. + */ + std::shared_ptr Get(TId id) const { + std::lock_guard lock(mutex_); + auto it = objects_.find(id); + return it == objects_.end() ? nullptr : it->second; + } + + /** + * @brief Check if an object exists for the given ID. + * + * This operation is O(1) average-case. + * + * @param id The identifier to check. + * @return true If an entry exists for the given ID. + * @return false If no entry exists for the given ID. + */ + bool Contains(TId id) const { + std::lock_guard lock(mutex_); + return objects_.find(id) != objects_.end(); + } + + /** + * @brief Get a snapshot vector of all stored objects. + * + * The returned vector contains strong references to the objects as they + * existed at the moment of the call. Subsequent mutations to the registry + * are not reflected in the returned vector. + * + * Complexity: O(N) to allocate and copy shared pointers. + * + * @return std::vector> Snapshot of all objects. + */ + std::vector> GetAll() const { + std::lock_guard lock(mutex_); + std::vector> result; + result.reserve(objects_.size()); + for (const auto& kv : objects_) { + result.push_back(kv.second); + } + return result; + } + + /** + * @brief Remove an object by ID. + * + * This operation is O(1) average-case. + * + * @param id The identifier to remove. + * @return true If an entry was found and removed. + * @return false If no entry existed for the given ID. + */ + bool Remove(TId id) { + std::lock_guard lock(mutex_); + return objects_.erase(id) > 0; + } + + /** + * @brief Remove all entries from the registry. + * + * Complexity: O(N) to destroy or release stored shared pointers. + */ + void Clear() { + std::lock_guard lock(mutex_); + objects_.clear(); + } + + private: + // Mutex is mutable to allow locking in logically-const operations. + mutable std::mutex mutex_; + std::unordered_map> objects_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/image.h b/packages/cnativeapi/cxx_impl/src/image.h new file mode 100644 index 0000000..c2683c8 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/image.h @@ -0,0 +1,236 @@ +#pragma once + +#include +#include +#include +#include +#include "foundation/geometry.h" +#include "foundation/native_object_provider.h" + +namespace nativeapi { + +/** + * @brief Image class for cross-platform image handling. + * + * This class provides a unified interface for working with images across + * different platforms. It supports multiple initialization methods including + * file paths, base64-encoded data, and system icons. + * + * The Image class is designed to be used with UI components like TrayIcon + * and MenuItem that require icon images. + * + * Features: + * - Load images from file paths + * - Load images from base64-encoded strings + * - Automatic format detection and conversion + * - Memory-efficient internal representation + * + * @note This class uses the PIMPL idiom to hide platform-specific + * implementation details and ensure binary compatibility. + * + * @note All Image instances must be created using static factory methods + * (FromFile, FromBase64). Empty/null images are represented + * using std::shared_ptr{nullptr}. + * + * @note Assignment operations are not supported to avoid resource management + * issues with platform-specific native objects. Use shared_ptr assignment + * instead: `auto newImage = oldImage;` + * + * @example + * ```cpp + * // Create image from file path + * auto image1 = Image::FromFile("/path/to/icon.png"); + * + * // Create image from base64 string + * auto image2 = Image::FromBase64("data:image/png;base64,iVBORw0KGgo..."); + * + * // Use with TrayIcon + * trayIcon->SetIcon(image1); + * + * // Use with MenuItem + * menuItem->SetIcon(image2); + * + * // Empty/null image representation + * std::shared_ptr emptyImage = nullptr; + * + * // Assignment using shared_ptr (recommended) + * auto newImage = image1; // Creates a new shared_ptr pointing to same object + * + * // Get image dimensions + * auto size = image1->GetSize(); + * if (size.width > 0 && size.height > 0) { + * std::cout << "Image size: " << size.width << "x" << size.height << + * std::endl; + * } + * + * // Get image format for debugging + * std::string format = image1->GetFormat(); + * std::cout << "Image format: " << format << std::endl; + * ``` + */ +class Image : public NativeObjectProvider { + public: + /** + * @brief Destructor. + * + * Cleans up the image and releases any associated platform-specific + * resources. + */ + ~Image(); + + /** + * @brief Copy constructor. + * + * Creates a copy of the image. The underlying image data may be shared + * between instances depending on the platform implementation. + * + * @param other The image to copy from + */ + Image(const Image& other); + + /** + * @brief Move constructor. + * + * Transfers ownership of the image data from another instance. + * + * @param other The image to move from + */ + Image(Image&& other) noexcept; + + /** + * @brief Create an image from a file path. + * + * Loads an image from the specified file path on disk. The image format + * is automatically detected based on the file contents. + * + * @param file_path Path to the image file + * @return A shared pointer to the created Image, or nullptr if loading failed + * + * @note Supported formats depend on the platform: + * - macOS: PNG, JPEG, GIF, TIFF, BMP, ICO, PDF + * - Windows: PNG, JPEG, BMP, GIF, TIFF, ICO + * - Linux: PNG, JPEG, BMP, GIF, SVG, XPM (depends on system libraries) + * + * @example + * ```cpp + * auto image = Image::FromFile("/path/to/icon.png"); + * if (image && image->IsValid()) { + * trayIcon->SetIcon(image); + * } + * ``` + */ + static std::shared_ptr FromFile(const std::string& file_path); + + /** + * @brief Create an image from base64-encoded data. + * + * Decodes and loads an image from a base64-encoded string. The string + * can optionally include a data URI prefix (e.g., "data:image/png;base64,"). + * + * @param base64_data Base64-encoded image data, with or without data URI + * prefix + * @return A shared pointer to the created Image, or nullptr if decoding + * failed + * + * @note The image format is automatically detected from the decoded data. + * + * @example + * ```cpp + * // With data URI prefix + * auto image1 = Image::FromBase64("data:image/png;base64,iVBORw0KGgo..."); + * + * // Without data URI prefix + * auto image2 = Image::FromBase64("iVBORw0KGgo..."); + * ``` + */ + static std::shared_ptr FromBase64(const std::string& base64_data); + + /** + * @brief Get the size of the image in pixels. + * + * @return The image size with width and height as double values, + * or Size(0,0) if the image is invalid + */ + Size GetSize() const; + + /** + * @brief Get the image format string for debugging purposes. + * + * @return The image format (e.g., "PNG", "JPEG", "GIF"), or empty string if + * unknown + */ + std::string GetFormat() const; + + /** + * @brief Convert the image to base64-encoded PNG data. + * + * Encodes the image as PNG and returns it as a base64 string with + * the data URI prefix. + * + * @return Base64-encoded PNG data with data URI prefix, or empty string on + * error + * + * @example + * ```cpp + * auto image = Image::FromFile("/path/to/icon.png"); + * std::string base64 = image->ToBase64(); + * // Result: "data:image/png;base64,iVBORw0KGgo..." + * ``` + */ + std::string ToBase64() const; + + /** + * @brief Save the image to a file. + * + * Saves the image to the specified file path. The format is determined + * by the file extension. + * + * @param file_path Path where the image should be saved + * @return true if saved successfully, false otherwise + * + * @note Supported output formats depend on the platform but typically + * include PNG, JPEG, BMP, and TIFF. + * + * @example + * ```cpp + * auto image = Image::FromBase64("data:image/png;base64,iVBORw0KGgo..."); + * image->SaveToFile("/path/to/output.png"); + * ``` + */ + bool SaveToFile(const std::string& file_path) const; + + protected: + /** + * @brief Internal method to get the platform-specific native image object. + * + * This method must be implemented by platform-specific code to return + * the underlying native image object. + * + * @return Pointer to the native image object + */ + void* GetNativeObjectInternal() const override; + + private: + /** + * @brief Private default constructor for use by factory methods. + * + * This constructor is private to prevent direct instantiation of Image objects. + * Use the static factory methods (FromFile, FromBase64, FromSystemIcon) instead. + */ + Image(); + + /** + * @brief Private implementation class using the PIMPL idiom. + * + * This forward declaration hides the platform-specific implementation + * details from the public interface. + */ + class Impl; + + /** + * @brief Pointer to the private implementation instance. + */ + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/keyboard_monitor.h b/packages/cnativeapi/cxx_impl/src/keyboard_monitor.h new file mode 100644 index 0000000..9885347 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/keyboard_monitor.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include "foundation/event_emitter.h" +#include "foundation/keyboard.h" + +namespace nativeapi { + +class KeyboardMonitor : public EventEmitter { + public: + KeyboardMonitor(); + virtual ~KeyboardMonitor(); + + // Start the keyboard monitor + void Start(); + + // Stop the keyboard monitor + void Stop(); + + // Check if the keyboard monitor is monitoring + bool IsMonitoring() const; + + // Get access to the event emitter for internal use + EventEmitter& GetInternalEventEmitter(); + + private: + class Impl; + std::unique_ptr impl_; + + friend class Impl; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/launch_at_login.h b/packages/cnativeapi/cxx_impl/src/launch_at_login.h new file mode 100644 index 0000000..86a1713 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/launch_at_login.h @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include + +namespace nativeapi { + +/** + * @brief Manage launching the application at user login (cross-platform). + * + * LaunchAtLogin provides a unified API to enable or disable starting your application + * automatically when the user logs in. The actual mechanism is platform-specific, + * but this class abstracts away those differences. + * + * Platform implementations: + * - Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run registry key + * - macOS: ServiceManagement SMAppService for the main app or bundled login item helpers + * - Linux (XDG): ~/.config/autostart/[app_id].desktop + * - Android/iOS/OHOS: Not supported (returns false from IsSupported/operations) + * + * Notes: + * - This API is intended for desktop environments. On mobile platforms this API is + * typically unsupported by design. Methods will fail gracefully. + * - You can let the implementation determine the current executable path, or call + * SetProgram() to customize the target binary and arguments recorded in the OS where the + * platform supports arbitrary launch commands. On macOS, SMAppService can only register + * the main app or a bundled helper, so custom executable paths and arguments are not + * supported. + * - Some platforms may require application-specific permissions or entitlements + * (e.g., sandbox restrictions on macOS). In such cases, operations may fail. + * + * Typical usage: + * @code + * using namespace nativeapi; + * + * if (LaunchAtLogin::IsSupported()) { + * LaunchAtLogin launch_at_login("com.example.myapp", "MyApp"); + * // Optionally override the program and arguments where supported: + * launch_at_login.SetProgram("/usr/local/bin/myapp", {"--minimized"}); + * + * launch_at_login.Enable(); + * bool enabled = launch_at_login.IsEnabled(); // should be true + * } + * @endcode + */ +class LaunchAtLogin { + public: + /** + * @brief Check whether launch-at-login is supported on this platform. + * + * @return true if supported; false for unsupported platforms (e.g., mobile). + */ + static bool IsSupported(); + + /** + * @brief Construct a LaunchAtLogin manager with default identifier and display name. + * + * The default identifier and display name are implementation-defined. Typically, + * the identifier is derived from the current process/bundle information, and the + * display name is derived from the application or executable name. + */ + LaunchAtLogin(); + + /** + * @brief Construct a LaunchAtLogin manager with a custom identifier. + * + * @param id A stable, unique identifier for your app. + * Examples: + * - Windows: used as the registry value name, e.g., "MyApp" + * - macOS: bundle identifier for a bundled LoginItem helper, e.g., + * "com.example.myapp.Helper"; the default constructor registers the main app + * - Linux: used as the .desktop file name (without extension), e.g., "myapp" + * + * Recommendation: Use a reverse-DNS identifier when possible, e.g., "com.example.myapp". + */ + explicit LaunchAtLogin(const std::string& id); + + /** + * @brief Construct a LaunchAtLogin manager with a custom identifier and display name. + * + * @param id Stable, unique identifier (see above). + * @param display_name Human-readable name shown in OS surfaces where applicable. + */ + LaunchAtLogin(const std::string& id, const std::string& display_name); + + virtual ~LaunchAtLogin(); + + /** + * @brief Get the unique identifier associated with this LaunchAtLogin instance. + * + * @return The identifier string. + */ + std::string GetId() const; + + /** + * @brief Get the human-readable display name used where applicable. + * + * @return The display name string. + */ + std::string GetDisplayName() const; + + /** + * @brief Set a human-readable display name used where applicable. + * + * Some platforms surface a name in their UI (e.g., Linux .desktop Name). + * + * @param display_name The human-readable name. + * @return true if the value was stored locally; does not change OS registration until Enable(). + */ + bool SetDisplayName(const std::string& display_name); + + /** + * @brief Set the program (executable) path and optional arguments used to launch at login. + * + * If not set, implementations will try to use the current process executable path. + * On platforms that require a single string (e.g., Windows registry), arguments will + * be encoded appropriately (quoted when needed). On Linux, arguments are stored in + * the .desktop Exec line. On macOS, SMAppService does not support arbitrary + * executable paths or arguments for main-app login items. + * + * @param executable_path Absolute path to the executable to run on login. + * @param arguments Optional arguments; order is preserved. + * @return true if stored locally; does not change OS registration until Enable(). + */ + bool SetProgram(const std::string& executable_path, + const std::vector& arguments = {}); + + /** + * @brief Get the currently configured executable path used to launch at login. + * + * This returns the locally configured value (not necessarily what is stored in the OS). + * If never set explicitly and cannot be resolved from the current process, it may be empty. + * + * @return Executable path string (may be empty). + */ + std::string GetExecutablePath() const; + + /** + * @brief Get the currently configured arguments used to launch at login. + * + * This returns the locally configured value (not necessarily what is stored in the OS). + * + * @return Vector of arguments (may be empty). + */ + std::vector GetArguments() const; + + /** + * @brief Enable launch-at-login for the configured program and arguments. + * + * If no program was explicitly set via SetProgram(), the implementation will attempt + * to resolve the current executable path and use that as the program to start. + * + * @return true on success; false on error or when unsupported. + */ + bool Enable(); + + /** + * @brief Disable launch-at-login. + * + * @return true on success; false on error or when unsupported. + */ + bool Disable(); + + /** + * @brief Query whether launch-at-login is currently enabled for this manager's identifier. + * + * This checks the platform-specific mechanism to determine whether the app (program path + * and arguments currently configured) is registered to start at user login. + * + * @return true if currently enabled; false otherwise. + */ + bool IsEnabled() const; + + // Prevent copying and moving + LaunchAtLogin(const LaunchAtLogin&) = delete; + LaunchAtLogin& operator=(const LaunchAtLogin&) = delete; + LaunchAtLogin(LaunchAtLogin&&) = delete; + LaunchAtLogin& operator=(LaunchAtLogin&&) = delete; + + private: + class Impl; + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/menu.cpp b/packages/cnativeapi/cxx_impl/src/menu.cpp new file mode 100644 index 0000000..aa01595 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/menu.cpp @@ -0,0 +1,3 @@ +#include "menu.h" + +namespace nativeapi {} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/menu.h b/packages/cnativeapi/cxx_impl/src/menu.h new file mode 100644 index 0000000..6eb3486 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/menu.h @@ -0,0 +1,746 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "foundation/event.h" +#include "foundation/event_emitter.h" +#include "foundation/geometry.h" +#include "foundation/id_allocator.h" +#include "foundation/keyboard.h" +#include "foundation/native_object_provider.h" +#include "placement.h" +#include "positioning_strategy.h" + +namespace nativeapi { + +class Image; + +typedef IdAllocator::IdType MenuId; +typedef IdAllocator::IdType MenuItemId; + +/** + * @brief Enumeration of different menu item types. + * + * Defines the various types of menu items that can be created, + * each with different behavior and appearance characteristics. + */ +enum class MenuItemType { + /** + * Normal clickable menu item with text and optional icon. + */ + Normal, + + /** + * Checkable menu item that can be toggled on/off. + */ + Checkbox, + + /** + * Radio button menu item, part of a mutually exclusive group. + */ + Radio, + + /** + * Separator line between menu items. + */ + Separator, + + /** + * Submenu item that expands to show child items. + */ + Submenu +}; + +/** + * @brief State of a menu item (for checkboxes and radio buttons). + * + * Defines the possible states for checkable menu items. + * Mixed state is typically used for checkboxes to indicate + * a partially selected or indeterminate state. + */ +enum class MenuItemState { + /** + * Item is not checked/selected. + */ + Unchecked, + + /** + * Item is checked/selected. + */ + Checked, + + /** + * Item is in mixed/indeterminate state (checkboxes only). + * Typically shown as a dash (-) or special symbol. + */ + Mixed +}; + +// Forward declarations +class Menu; + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * @brief Base class for all menu-related events. + * + * This class provides common functionality for menu events. + */ +class MenuEvent : public Event { + public: + virtual ~MenuEvent() = default; + + std::string GetTypeName() const override { return "MenuEvent"; } +}; + +/** + * @brief Menu opened event. + * + * This event is fired when a menu has been displayed. + */ +class MenuOpenedEvent : public MenuEvent { + public: + MenuOpenedEvent(MenuId menu_id) : menu_id_(menu_id) {} + + MenuId GetMenuId() const { return menu_id_; } + + std::string GetTypeName() const override { return "MenuOpenedEvent"; } + + private: + MenuId menu_id_; +}; + +/** + * @brief Menu closed event. + * + * This event is fired when a menu has been hidden or closed. + */ +class MenuClosedEvent : public MenuEvent { + public: + MenuClosedEvent(MenuId menu_id) : menu_id_(menu_id) {} + + MenuId GetMenuId() const { return menu_id_; } + + std::string GetTypeName() const override { return "MenuClosedEvent"; } + + private: + MenuId menu_id_; +}; + +/** + * @brief Menu item clicked event. + * + * This event is fired when a menu item is clicked or activated. + * Contains information about which menu item was clicked. + */ +class MenuItemClickedEvent : public MenuEvent { + public: + MenuItemClickedEvent(MenuItemId item_id) : item_id_(item_id) {} + + MenuItemId GetItemId() const { return item_id_; } + + std::string GetTypeName() const override { return "MenuItemClickedEvent"; } + + private: + MenuItemId item_id_; +}; + +/** + * @brief Menu item submenu opened event. + * + * This event is fired when a menu item's submenu has been displayed. + */ +class MenuItemSubmenuOpenedEvent : public MenuEvent { + public: + MenuItemSubmenuOpenedEvent(MenuItemId item_id) : item_id_(item_id) {} + + MenuItemId GetItemId() const { return item_id_; } + + std::string GetTypeName() const override { return "MenuItemSubmenuOpenedEvent"; } + + private: + MenuItemId item_id_; +}; + +/** + * @brief Menu item submenu closed event. + * + * This event is fired when a menu item's submenu has been hidden or closed. + */ +class MenuItemSubmenuClosedEvent : public MenuEvent { + public: + MenuItemSubmenuClosedEvent(MenuItemId item_id) : item_id_(item_id) {} + + MenuItemId GetItemId() const { return item_id_; } + + std::string GetTypeName() const override { return "MenuItemSubmenuClosedEvent"; } + + private: + MenuItemId item_id_; +}; + +/** + * @brief MenuItem represents a single item in a menu. + * + * This class provides a cross-platform interface for creating and managing + * menu items. Menu items can be simple clickable items, checkboxes, radio + * buttons, separators, or submenu containers. + * + * The class supports: + * - Different item types (normal, checkbox, radio, separator, submenu) + * - Custom text, icons, and tooltips + * - Keyboard shortcuts/accelerators + * - Enable/disable state + * - Event emission for user interaction + * - Submenu nesting + * + * @note This class uses the PIMPL idiom to hide platform-specific + * implementation details and ensure binary compatibility across different + * platforms. It also inherits from EventEmitter to provide event-driven + * interaction handling. + * + * @example + * ```cpp + * // Create a normal menu item + * auto item = std::make_shared("Open File", MenuItemType::Normal); + * item->SetIcon("data:image/png;base64,..."); + * item->SetAccelerator(KeyboardAccelerator("O", ModifierKey::Ctrl)); + * item->AddListener([](const MenuItemClickedEvent& event) + * { + * // Handle menu item click + * std::cout << "Opening file..." << std::endl; + * }); + * + * // Create a checkbox item + * auto checkbox = std::make_shared("Show Toolbar", MenuItemType::Checkbox); + * checkbox->SetState(MenuItemState::Checked); + * checkbox->AddListener([](const MenuItemClickedEvent& + * event) { std::cout << "Toolbar clicked, handle state change manually" << + * std::endl; + * }); + * ``` + */ +class MenuItem : public EventEmitter, public NativeObjectProvider { + public: + /** + * @brief Constructor to create a new menu item. + * + * Creates a menu item of the specified type with the given text. + * + * @param label The display text for the menu item + * @param type The type of menu item to create + * + * @example + * ```cpp + * auto item = std::make_shared("File", MenuItemType::Normal); + * auto separator = std::make_shared("", MenuItemType::Separator); + * auto checkbox = std::make_shared("Word Wrap", MenuItemType::Checkbox); + * ``` + */ + MenuItem(const std::string& label = "", MenuItemType type = MenuItemType::Normal); + + /** + * @brief Constructor that wraps an existing platform-specific menu item. + * + * This constructor is typically used internally by platform-specific + * implementations to wrap existing menu items. + * + * @param native_item Pointer to the platform-specific menu item object + */ + explicit MenuItem(void* native_item); + + /** + * @brief Destructor for MenuItem. + * + * Cleans up the menu item and releases any associated platform-specific + * resources. Also removes any event listeners. + */ + virtual ~MenuItem(); + + /** + * @brief Get the unique identifier for this menu item. + * + * This ID is assigned when the item is created and can be used to + * reference the item in event handlers and other operations. + * + * @return The unique identifier for this menu item + */ + MenuItemId GetId() const; + + /** + * @brief Get the type of this menu item. + * + * @return The MenuItemType of this item + */ + MenuItemType GetType() const; + + /** + * @brief Set the display label for the menu item. + * + * The label is what appears in the menu. On some platforms, + * ampersand characters (&) can be used to indicate mnemonics + * (keyboard navigation keys). + * + * @param label The label to display (optional) + * + * @example + * ```cpp + * item->SetLabel("&File"); // 'F' becomes the mnemonic on Windows/Linux + * item->SetLabel("Open Recent"); + * item->SetLabel(std::nullopt); // Clear the label + * ``` + */ + void SetLabel(const std::optional& label); + + /** + * @brief Get the current display label of the menu item. + * + * @return The current label as an optional string + */ + std::optional GetLabel() const; + + /** + * @brief Set the icon for the menu item using an Image object. + * + * This is the preferred method for setting the menu item icon as it + * provides type safety and better control over image handling. + * + * @param image Shared pointer to an Image object, or nullptr to clear the icon + * + * @example + * ```cpp + * // Using file path + * auto icon = Image::FromFile("/path/to/icon.png"); + * item->SetIcon(icon); + * + * // Using base64 data + * auto icon = Image::FromBase64("data:image/png;base64,iVBORw0KGgo..."); + * item->SetIcon(icon); + * + * // Using system icon + * auto icon = Image::FromSystemIcon("folder"); + * item->SetIcon(icon); + * + * // Clear icon + * item->SetIcon(nullptr); + * ``` + */ + void SetIcon(std::shared_ptr image); + + /** + * @brief Get the current icon image of the menu item. + * + * @return A shared pointer to the current Image object, or nullptr if no icon is set + */ + std::shared_ptr GetIcon() const; + + /** + * @brief Set the tooltip text for the menu item. + * + * The tooltip may appear when the user hovers over the menu item, + * depending on the platform and menu context. + * + * @param tooltip The tooltip text to display (optional) + */ + void SetTooltip(const std::optional& tooltip); + + /** + * @brief Get the current tooltip text of the menu item. + * + * @return The current tooltip text as an optional string + */ + std::optional GetTooltip() const; + + /** + * @brief Set the keyboard accelerator for the menu item. + * + * The accelerator allows users to trigger the menu item using + * keyboard shortcuts. The accelerator is typically displayed + * next to the menu item label. + * + * @param accelerator The keyboard accelerator to set, or std::nullopt to remove + * + * @example + * ```cpp + * // Set Ctrl+S as accelerator + * item->SetAccelerator(KeyboardAccelerator("S", ModifierKey::Ctrl)); + * + * // Set F1 as accelerator + * item->SetAccelerator(KeyboardAccelerator("F1")); + * + * // Set Alt+F4 as accelerator + * item->SetAccelerator(KeyboardAccelerator("F4", ModifierKey::Alt)); + * + * // Remove accelerator + * item->SetAccelerator(std::nullopt); + * ``` + */ + void SetAccelerator(const std::optional& accelerator); + + /** + * @brief Get the current keyboard accelerator of the menu item. + * + * @return The current KeyboardAccelerator, or an empty accelerator if none is + * set + */ + KeyboardAccelerator GetAccelerator() const; + + /** + * @brief Enable or disable the menu item. + * + * Disabled menu items are typically grayed out and cannot be clicked. + * + * @param enabled true to enable the item, false to disable it + */ + void SetEnabled(bool enabled); + + /** + * @brief Check if the menu item is currently enabled. + * + * @return true if the item is enabled, false if disabled + */ + bool IsEnabled() const; + + /** + * @brief Set the state of a checkbox or radio menu item. + * + * This method allows you to set the checked state, including + * you to set mixed/indeterminate state for checkboxes. + * For radio items, only Unchecked and Checked states are valid. + * + * @param state The desired state (Unchecked, Checked, or Mixed) + */ + void SetState(MenuItemState state); + + /** + * @brief Get the current state of a checkbox or radio menu item. + * + * @return The current state (Unchecked, Checked, or Mixed) + */ + MenuItemState GetState() const; + + /** + * @brief Set the radio group ID for radio menu items. + * + * Radio items with the same group ID are mutually exclusive - + * only one item in the group can be checked at a time. + * + * @param group_id The radio group identifier + */ + void SetRadioGroup(int group_id); + + /** + * @brief Get the radio group ID of this menu item. + * + * @return The radio group ID, or -1 if not a radio item or no group set + */ + int GetRadioGroup() const; + + /** + * @brief Set the submenu for this menu item. + * + * This converts the item into a submenu item that expands to show + * the provided menu when hovered or clicked. + * + * @param submenu Shared pointer to the submenu to attach + */ + void SetSubmenu(std::shared_ptr submenu); + + /** + * @brief Get the submenu attached to this menu item. + * + * @return Shared pointer to the submenu, or nullptr if no submenu is attached + */ + std::shared_ptr GetSubmenu() const; + + protected: + /** + * @brief Internal method to get the platform-specific native menu item object. + * + * This method must be implemented by platform-specific code to return + * the underlying native menu item object. + * + * @return Pointer to the native menu item object + */ + void* GetNativeObjectInternal() const override; + + private: + friend class Menu; + + /** + * @brief Private implementation class using the PIMPL idiom. + */ + class Impl; + + /** + * @brief Pointer to the private implementation instance. + */ + std::unique_ptr pimpl_; +}; + +/** + * @brief Menu represents a collection of menu items. + * + * This class provides a cross-platform interface for creating and managing + * menus. Menus can contain various types of items including normal items, + * checkboxes, radio buttons, separators, and submenus. + * + * The class supports: + * - Adding, removing, and organizing menu items + * - Displaying as context menus or application menus + * - Event emission for menu interactions + * - Hierarchical submenu structures + * - Dynamic menu modification + * + * @note This class uses the PIMPL idiom to hide platform-specific + * implementation details and ensure binary compatibility across different + * platforms. It also inherits from EventEmitter to provide event-driven + * interaction handling. + * + * @example + * ```cpp + * // Create a file menu + * auto file_menu = std::make_shared(); + * + * // Add items to the menu + * auto new_item = std::make_shared("New", MenuItemType::Normal); + * new_item->SetAccelerator(KeyboardAccelerator("N", ModifierKey::Ctrl)); + * file_menu->AddItem(new_item); + * + * auto open_item = std::make_shared("Open", MenuItemType::Normal); + * open_item->SetAccelerator(KeyboardAccelerator("O", + * ModifierKey::Ctrl)); file_menu->AddItem(open_item); + * + * file_menu->AddSeparator(); + * + * auto exit_item = std::make_shared("Exit", MenuItemType::Normal); + * file_menu->AddItem(exit_item); + * + * // Listen to menu events + * file_menu->AddListener([](const MenuOpenedEvent& event) { + * std::cout << "Menu opened" << std::endl; + * }); + * + * // Open as context menu + * file_menu->Open(100, 100); + * ``` + */ +class Menu : public EventEmitter, public NativeObjectProvider { + public: + /** + * @brief Constructor to create a new menu. + * + * Creates an empty menu that can be populated with menu items. + */ + Menu(); + + /** + * @brief Constructor that wraps an existing platform-specific menu. + * + * This constructor is typically used internally by platform-specific + * implementations to wrap existing menus. + * + * @param native_menu Pointer to the platform-specific menu object + */ + explicit Menu(void* native_menu); + + /** + * @brief Destructor for Menu. + * + * Cleans up the menu and all its items, releasing any associated + * platform-specific resources. + */ + virtual ~Menu(); + + /** + * @brief Get the unique identifier for this menu. + * + * This ID is assigned when the menu is created and can be used to + * reference the menu in various operations. + * + * @return The unique identifier for this menu + */ + MenuId GetId() const; + + /** + * @brief Add a menu item to the end of the menu. + * + * The item is added to the bottom of the menu's item list. + * + * @param item Shared pointer to the menu item to add + * + * @example + * ```cpp + * auto item = std::make_shared("Save"); + * menu->AddItem(item); + * ``` + */ + void AddItem(std::shared_ptr item); + + /** + * @brief Insert a menu item at a specific position. + * + * Inserts the item at the specified index, shifting existing items + * to make room. If the index is out of bounds, the item is added + * to the end. + * + * @param index The position where to insert the item (0-based) + * @param item Shared pointer to the menu item to insert + */ + void InsertItem(size_t index, std::shared_ptr item); + + /** + * @brief Remove a menu item from the menu. + * + * Removes the specified item from the menu if it exists. + * + * @param item Shared pointer to the menu item to remove + * @return true if the item was found and removed, false otherwise + */ + bool RemoveItem(std::shared_ptr item); + + /** + * @brief Remove a menu item by its ID. + * + * Removes the menu item with the specified ID from the menu. + * + * @param item_id The ID of the menu item to remove + * @return true if the item was found and removed, false otherwise + */ + bool RemoveItemById(MenuItemId item_id); + + /** + * @brief Remove a menu item at a specific position. + * + * Removes the menu item at the specified index. + * + * @param index The position of the item to remove (0-based) + * @return true if the item was removed, false if index was out of bounds + */ + bool RemoveItemAt(size_t index); + + /** + * @brief Remove all menu items from the menu. + * + * Clears the entire menu, removing all items. + */ + void Clear(); + + /** + * @brief Add a separator line to the menu. + * + * Separators are used to visually group related menu items. + * This is a convenience method equivalent to adding a separator MenuItem. + */ + void AddSeparator(); + + /** + * @brief Insert a separator at a specific position. + * + * @param index The position where to insert the separator (0-based) + */ + void InsertSeparator(size_t index); + + /** + * @brief Get the number of items in the menu. + * + * @return The total number of menu items (including separators) + */ + size_t GetItemCount() const; + + /** + * @brief Get a menu item by its position. + * + * Returns the menu item at the specified index. + * + * @param index The position of the item to retrieve (0-based) + * @return Shared pointer to the menu item, or nullptr if index is out of + * bounds + */ + std::shared_ptr GetItemAt(size_t index) const; + + /** + * @brief Get a menu item by its ID. + * + * Searches for and returns the menu item with the specified ID. + * + * @param item_id The ID of the menu item to find + * @return Shared pointer to the menu item, or nullptr if not found + */ + std::shared_ptr GetItemById(MenuItemId item_id) const; + + /** + * @brief Get all menu items in the menu. + * + * Returns a vector containing all menu items in order. + * + * @return Vector of shared pointers to all menu items + */ + std::vector> GetAllItems() const; + + /** + * @brief Display the menu as a context menu using the specified positioning strategy. + * + * Shows the menu according to the provided positioning strategy and waits for + * user interaction. The menu will close when the user clicks outside of it or + * selects an item. + * + * @param strategy The positioning strategy determining where to display the menu + * @param placement The placement option determining how the menu is positioned + * relative to the reference point (default: BottomStart) + * @return true if the menu was successfully opened, false otherwise + * + * @example + * ```cpp + * // Open context menu at cursor position, below the cursor + * menu->Open(PositioningStrategy::CursorPosition(), Placement::BottomStart); + * + * // Open context menu at specific coordinates, above and centered + * menu->Open(PositioningStrategy::Absolute({100, 200}), Placement::Top); + * + * // Open context menu relative to a button with offset, to the right + * Rectangle buttonRect = button->GetBounds(); + * menu->Open(PositioningStrategy::Relative(buttonRect, {0, 10}), Placement::Right); + * + * // Use default placement (BottomStart) + * menu->Open(PositioningStrategy::CursorPosition()); + * ``` + */ + bool Open(const PositioningStrategy& strategy, Placement placement = Placement::BottomStart); + + /** + * @brief Programmatically close the menu if it's currently showing. + * + * @return true if the menu was successfully closed, false otherwise + */ + bool Close(); + + protected: + /** + * @brief Internal method to get the platform-specific native menu object. + * + * This method must be implemented by platform-specific code to return + * the underlying native menu object. + * + * @return Pointer to the native menu object + */ + void* GetNativeObjectInternal() const override; + + private: + /** + * @brief Private implementation class using the PIMPL idiom. + */ + class Impl; + + /** + * @brief Pointer to the private implementation instance. + */ + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/message_dialog.h b/packages/cnativeapi/cxx_impl/src/message_dialog.h new file mode 100644 index 0000000..df37d65 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/message_dialog.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include "dialog.h" + +namespace nativeapi { + +/** + * @class MessageDialog + * @brief Dialog for displaying messages and simple prompts. + * + * MessageDialog is used to display information, warnings, errors, or + * questions to the user. It can be shown modally or non-modally. + * + * This class inherits from Dialog and provides message-specific + * functionality such as message text. + * + * @note This class uses the PIMPL idiom to hide platform-specific + * implementation details. + * + * @example + * ```cpp + * // Simple message dialog + * auto dialog = std::make_shared( + * "Update Available", + * "A new version is available. Would you like to update?"); + * dialog->SetModality(DialogModality::Application); + * dialog->Open(); + * ``` + */ +class MessageDialog : public Dialog { + public: + /** + * @brief Create a message dialog with title and message. + * + * @param title Dialog title + * @param message Dialog message + * + * @example + * ```cpp + * auto dialog = std::make_shared( + * "Update Available", + * "A new version is available. Would you like to update?"); + * dialog->SetModality(DialogModality::Application); + * dialog->Open(); + * ``` + */ + MessageDialog(const std::string& title, const std::string& message); + + /** + * @brief Destructor. + */ + virtual ~MessageDialog(); + + /** + * @brief Set the dialog title. + * + * @param title The dialog title + */ + void SetTitle(const std::string& title); + + /** + * @brief Get the dialog title. + * + * @return The current title + */ + std::string GetTitle() const; + + /** + * @brief Set the dialog message. + * + * @param message The dialog message + */ + void SetMessage(const std::string& message); + + /** + * @brief Get the dialog message. + * + * @return The current message + */ + std::string GetMessage() const; + + /** + * @brief Get the current modality setting of the dialog. + * + * @return The current DialogModality setting + */ + DialogModality GetModality() const override; + + /** + * @brief Set the modality of the dialog. + * + * @param modality The modality type to set + */ + void SetModality(DialogModality modality) override; + + /** + * @brief Open the dialog. + * + * Displays the dialog according to the current modality setting. + * + * @return true if the dialog was successfully opened, false otherwise + */ + bool Open() override; + + /** + * @brief Close the dialog programmatically. + * + * Dismisses the dialog as if the user had closed it. + * + * @return true if the dialog was successfully closed, false otherwise + */ + bool Close() override; + + private: + /** + * @brief Private implementation class. + */ + class Impl; + + /** + * @brief Pointer to platform-specific implementation. + * + * @note This is separate from Dialog::pimpl_ to allow for + * message-dialog-specific implementation details. + */ + std::unique_ptr pimpl_; + + /** + * @brief Current modality setting. + */ + DialogModality modality_ = DialogModality::None; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/placement.h b/packages/cnativeapi/cxx_impl/src/placement.h new file mode 100644 index 0000000..3cd2997 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/placement.h @@ -0,0 +1,95 @@ +#pragma once + +namespace nativeapi { + +/** + * @brief Placement options for positioning UI elements relative to an anchor. + * + * Defines how a UI element (such as a menu, tooltip, or popover) should be + * positioned relative to an anchor element or point. The placement consists + * of a primary direction (top, right, bottom, left) and an optional alignment + * (start, center, end). + * + * Primary directions: + * - Top: Element appears above the anchor + * - Right: Element appears to the right of the anchor + * - Bottom: Element appears below the anchor + * - Left: Element appears to the left of the anchor + * + * Alignments: + * - Start: Element aligns to the start edge (left for horizontal, top for vertical) + * - Center: Element centers along the anchor (default if not specified) + * - End: Element aligns to the end edge (right for horizontal, bottom for vertical) + * + * @example + * ```cpp + * // Position menu below the button, aligned to the left + * menu->Open(PositioningStrategy::Absolute({100, 100}), Placement::BottomStart); + * + * // Position popover to the right, aligned to the top + * popover->Open(PositioningStrategy::CursorPosition(), Placement::RightStart); + * ``` + */ +enum class Placement { + /** + * Position above the anchor, horizontally centered. + */ + Top, + + /** + * Position above the anchor, aligned to the start (left). + */ + TopStart, + + /** + * Position above the anchor, aligned to the end (right). + */ + TopEnd, + + /** + * Position to the right of the anchor, vertically centered. + */ + Right, + + /** + * Position to the right of the anchor, aligned to the start (top). + */ + RightStart, + + /** + * Position to the right of the anchor, aligned to the end (bottom). + */ + RightEnd, + + /** + * Position below the anchor, horizontally centered. + */ + Bottom, + + /** + * Position below the anchor, aligned to the start (left). + */ + BottomStart, + + /** + * Position below the anchor, aligned to the end (right). + */ + BottomEnd, + + /** + * Position to the left of the anchor, vertically centered. + */ + Left, + + /** + * Position to the left of the anchor, aligned to the start (top). + */ + LeftStart, + + /** + * Position to the left of the anchor, aligned to the end (bottom). + */ + LeftEnd +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/accessibility_manager_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/accessibility_manager_android.cpp new file mode 100644 index 0000000..3303257 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/accessibility_manager_android.cpp @@ -0,0 +1,15 @@ +#include "../../accessibility_manager.h" + +namespace nativeapi { + +void AccessibilityManager::Enable() { + // On Android, accessibility features are controlled by system settings + enabled_ = true; +} + +bool AccessibilityManager::IsEnabled() { + // On Android, accessibility features are controlled by system settings + return enabled_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/application_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/application_android.cpp new file mode 100644 index 0000000..b781165 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/application_android.cpp @@ -0,0 +1,68 @@ +#include +#include "../../application.h" +#include "../../window_manager.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class Application::Impl { + public: + Impl() {} +}; + +Application::Application() : pimpl_(std::make_unique()) {} +Application::~Application() {} + +int Application::Run() { + ALOGW("Application::Run not applicable on Android (handled by Activity lifecycle)"); + return 0; +} + +int Application::Run(std::shared_ptr window) { + ALOGW("Application::Run with window not applicable on Android"); + return 0; +} + +void Application::Quit(int exit_code) { + ALOGW("Application::Quit requests Activity finish"); +} + +bool Application::IsRunning() const { + return true; +} + +bool Application::IsSingleInstance() const { + return false; +} + +bool Application::SetIcon(const std::string& icon_path) { + ALOGW("Application::SetIcon not implemented on Android"); + return false; +} + +bool Application::SetDockIconVisible(bool visible) { + ALOGW("Application::SetDockIconVisible not applicable on Android"); + return false; +} + +bool Application::SetMenuBar(std::shared_ptr menu) { + ALOGW("Application::SetMenuBar not implemented on Android"); + return false; +} + +std::shared_ptr Application::GetPrimaryWindow() const { + return nullptr; +} + +void Application::SetPrimaryWindow(std::shared_ptr window) { + ALOGW("Application::SetPrimaryWindow not implemented on Android"); +} + +std::vector> Application::GetAllWindows() const { + auto& window_manager = WindowManager::GetInstance(); + return window_manager.GetAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/dispatcher_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/dispatcher_android.cpp new file mode 100644 index 0000000..2333c9c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/dispatcher_android.cpp @@ -0,0 +1,45 @@ +#include "../../foundation/dispatcher_platform.h" +#include "../../foundation/dispatcher_common.h" + +#include + +namespace nativeapi { +namespace dispatcher_platform { + +bool PlatformIsMainThread() { + return dispatcher_internal::IsMainThreadByCapturedId(); +} + +void PlatformSetMainThread() { + dispatcher_internal::CaptureCallerAsMainThread(); +} + +bool PlatformIsMainThreadDispatchSupported() { + return false; +} + +bool PlatformRunOnMainThread(std::function fn) { + // TODO(android): implement via ALooper. + // + // Sketch: on the Java UI thread, ALooper_forThread() yields the main looper. + // Create an eventfd (or pipe) and register it with ALooper_addFd(); posting + // then means pushing the callable onto a mutex-guarded queue and writing one + // byte to wake the looper, whose callback drains the queue. + // + // Reporting false is deliberate: silently dropping the callable would make + // events vanish with no diagnostic, which is how the pre-existing "event + // delivered on the wrong thread" bugs went unnoticed for so long. + (void)fn; + __android_log_print(ANDROID_LOG_WARN, "NativeApi", + "RunOnMainThread is not implemented on Android; work was not run."); + return false; +} + +bool PlatformRunMainThreadLoopFor(int timeout_ms) { + // Nothing to service until PlatformRunOnMainThread() is implemented. + (void)timeout_ms; + return false; +} + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/display_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/display_android.cpp new file mode 100644 index 0000000..f9fa052 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/display_android.cpp @@ -0,0 +1,63 @@ +#include +#include "../../display.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class Display::Impl { + public: + Impl() {} + + const DisplayId id_ = IdAllocator::Allocate(); +}; + +Display::Display(void* display) : pimpl_(std::make_unique()) {} +Display::~Display() {} + +void* Display::GetNativeObjectInternal() const { + return nullptr; +} + +DisplayId Display::GetId() const { + return pimpl_->id_; +} + +std::string Display::GetName() const { + return "Android Display"; +} + +Point Display::GetPosition() const { + return Point{0, 0}; +} + +Size Display::GetSize() const { + return Size{1080, 1920}; +} + +Rectangle Display::GetWorkArea() const { + return Rectangle{0, 0, 1080, 1920}; +} + +double Display::GetScaleFactor() const { + return 2.0; +} + +bool Display::IsPrimary() const { + return true; +} + +DisplayOrientation Display::GetOrientation() const { + return DisplayOrientation::kPortrait; +} + +int Display::GetRefreshRate() const { + return 60; +} + +int Display::GetBitDepth() const { + return 24; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/display_manager_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/display_manager_android.cpp new file mode 100644 index 0000000..51d274d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/display_manager_android.cpp @@ -0,0 +1,21 @@ +#include +#include "../../display_manager.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +DisplayManager::DisplayManager() {} +DisplayManager::~DisplayManager() {} + +std::vector DisplayManager::EnumerateNativeDisplays() { + // Stub: a single default display. + return {{"android_display_0", nullptr, true}}; +} + +Point DisplayManager::GetCursorPosition() { + return Point{0, 0}; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/image_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/image_android.cpp new file mode 100644 index 0000000..212b10b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/image_android.cpp @@ -0,0 +1,51 @@ +#include +#include "../../image.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class Image::Impl { + public: + Impl() {} +}; + +Image::Image() : pimpl_(std::make_unique()) {} +Image::~Image() {} +Image::Image(const Image& other) : pimpl_(std::make_unique()) {} +Image::Image(Image&& other) noexcept : pimpl_(std::move(other.pimpl_)) {} + +std::shared_ptr Image::FromFile(const std::string& file_path) { + ALOGW("Image::FromFile not implemented on Android"); + return nullptr; +} + +std::shared_ptr Image::FromBase64(const std::string& base64_data) { + ALOGW("Image::FromBase64 not implemented on Android"); + return nullptr; +} + +Size Image::GetSize() const { + return Size{0, 0}; +} + +std::string Image::GetFormat() const { + return ""; +} + +std::string Image::ToBase64() const { + ALOGW("Image::ToBase64 not implemented on Android"); + return ""; +} + +bool Image::SaveToFile(const std::string& file_path) const { + ALOGW("Image::SaveToFile not implemented on Android"); + return false; +} + +void* Image::GetNativeObjectInternal() const { + return nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/keyboard_monitor_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/keyboard_monitor_android.cpp new file mode 100644 index 0000000..d07f939 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/keyboard_monitor_android.cpp @@ -0,0 +1,34 @@ +#include +#include "../../keyboard_monitor.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class KeyboardMonitor::Impl { + public: + Impl(KeyboardMonitor* monitor) : monitor_(monitor) {} + KeyboardMonitor* monitor_; +}; + +KeyboardMonitor::KeyboardMonitor() : impl_(std::make_unique(this)) {} +KeyboardMonitor::~KeyboardMonitor() {} + +void KeyboardMonitor::Start() { + ALOGW("KeyboardMonitor::Start requires AccessibilityService on Android"); +} + +void KeyboardMonitor::Stop() { + ALOGW("KeyboardMonitor::Stop stops monitoring"); +} + +bool KeyboardMonitor::IsMonitoring() const { + return false; +} + +EventEmitter& KeyboardMonitor::GetInternalEventEmitter() { + return *this; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/launch_at_login_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/launch_at_login_android.cpp new file mode 100644 index 0000000..b9f666c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/launch_at_login_android.cpp @@ -0,0 +1,104 @@ +#include "../../launch_at_login.h" + +namespace nativeapi { + +/** + * Android stub implementation for LaunchAtLogin. + * + * Auto-start at user login is not supported on Android. All operations that would + * enable/disable or configure launch-at-login return false. Getters return the locally + * stored values (typically empty), while setters return false and do not modify state. + */ +class LaunchAtLogin::Impl { + public: + // Unsupported platform semantics + static bool IsSupported() { return false; } + + Impl() = default; + + explicit Impl(const std::string& id) : id_(id) {} + + Impl(const std::string& id, const std::string& display_name) + : id_(id), display_name_(display_name) {} + + ~Impl() = default; + + // Getters return whatever is locally available (likely empty) + std::string GetId() const { return id_; } + std::string GetDisplayName() const { return display_name_; } + + // No-op setter; returns false to indicate unsupported + bool SetDisplayName(const std::string& /*display_name*/) { return false; } + + bool SetProgram(const std::string& /*executable_path*/, + const std::vector& /*arguments*/) { + return false; + } + + std::string GetExecutablePath() const { return program_path_; } + std::vector GetArguments() const { return arguments_; } + + bool Enable() { return false; } + bool Disable() { return false; } + bool IsEnabled() const { return false; } + + private: + std::string id_; + std::string display_name_; + std::string program_path_; + std::vector arguments_; +}; + +// LaunchAtLogin public API forwarding to Impl + +LaunchAtLogin::LaunchAtLogin() : pimpl_(std::make_unique()) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id) : pimpl_(std::make_unique(id)) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id, const std::string& display_name) + : pimpl_(std::make_unique(id, display_name)) {} + +LaunchAtLogin::~LaunchAtLogin() = default; + +bool LaunchAtLogin::IsSupported() { + return Impl::IsSupported(); +} + +std::string LaunchAtLogin::GetId() const { + return pimpl_->GetId(); +} + +std::string LaunchAtLogin::GetDisplayName() const { + return pimpl_->GetDisplayName(); +} + +bool LaunchAtLogin::SetDisplayName(const std::string& display_name) { + return pimpl_->SetDisplayName(display_name); +} + +bool LaunchAtLogin::SetProgram(const std::string& executable_path, + const std::vector& arguments) { + return pimpl_->SetProgram(executable_path, arguments); +} + +std::string LaunchAtLogin::GetExecutablePath() const { + return pimpl_->GetExecutablePath(); +} + +std::vector LaunchAtLogin::GetArguments() const { + return pimpl_->GetArguments(); +} + +bool LaunchAtLogin::Enable() { + return pimpl_->Enable(); +} + +bool LaunchAtLogin::Disable() { + return pimpl_->Disable(); +} + +bool LaunchAtLogin::IsEnabled() const { + return pimpl_->IsEnabled(); +} + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/menu_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/menu_android.cpp new file mode 100644 index 0000000..dc0e802 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/menu_android.cpp @@ -0,0 +1,87 @@ +#include +#include "../../menu.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class Menu::Impl { + public: + Impl() {} +}; + +Menu::Menu() : pimpl_(std::make_unique()) {} +Menu::Menu(void* native_menu) : pimpl_(std::make_unique()) {} +Menu::~Menu() {} + +void* Menu::GetNativeObjectInternal() const { + return nullptr; +} + +MenuId Menu::GetId() const { + return IdAllocator::kInvalidId; +} + +void Menu::AddItem(std::shared_ptr item) { + ALOGW("Menu::AddItem not fully implemented on Android"); +} + +void Menu::InsertItem(size_t index, std::shared_ptr item) { + ALOGW("Menu::InsertItem not implemented on Android"); +} + +bool Menu::RemoveItem(std::shared_ptr item) { + ALOGW("Menu::RemoveItem not implemented on Android"); + return false; +} + +bool Menu::RemoveItemById(MenuItemId item_id) { + ALOGW("Menu::RemoveItemById not implemented on Android"); + return false; +} + +bool Menu::RemoveItemAt(size_t index) { + ALOGW("Menu::RemoveItemAt not implemented on Android"); + return false; +} + +void Menu::Clear() { + ALOGW("Menu::Clear not implemented on Android"); +} + +void Menu::AddSeparator() { + ALOGW("Menu::AddSeparator not implemented on Android"); +} + +void Menu::InsertSeparator(size_t index) { + ALOGW("Menu::InsertSeparator not implemented on Android"); +} + +size_t Menu::GetItemCount() const { + return 0; +} + +std::shared_ptr Menu::GetItemAt(size_t index) const { + return nullptr; +} + +std::shared_ptr Menu::GetItemById(MenuItemId item_id) const { + return nullptr; +} + +std::vector> Menu::GetAllItems() const { + return {}; +} + +bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { + ALOGW("Menu::Open not implemented on Android"); + return false; +} + +bool Menu::Close() { + ALOGW("Menu::Close not implemented on Android"); + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/menu_item_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/menu_item_android.cpp new file mode 100644 index 0000000..5239a0e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/menu_item_android.cpp @@ -0,0 +1,98 @@ +#include +#include "../../menu.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class MenuItem::Impl { + public: + Impl() {} +}; + +MenuItem::MenuItem(const std::string& label, MenuItemType type) : pimpl_(std::make_unique()) { + ALOGW("MenuItem created on Android"); +} + +MenuItem::MenuItem(void* native_item) : pimpl_(std::make_unique()) {} + +MenuItem::~MenuItem() {} + +void* MenuItem::GetNativeObjectInternal() const { + return nullptr; +} + +MenuItemId MenuItem::GetId() const { + return IdAllocator::kInvalidId; +} + +MenuItemType MenuItem::GetType() const { + return MenuItemType::Normal; +} + +void MenuItem::SetLabel(const std::optional& label) { + ALOGW("MenuItem::SetLabel not implemented on Android"); +} + +std::optional MenuItem::GetLabel() const { + return std::nullopt; +} + +void MenuItem::SetIcon(std::shared_ptr image) { + ALOGW("MenuItem::SetIcon not implemented on Android"); +} + +std::shared_ptr MenuItem::GetIcon() const { + return nullptr; +} + +void MenuItem::SetTooltip(const std::optional& tooltip) { + ALOGW("MenuItem::SetTooltip not implemented on Android"); +} + +std::optional MenuItem::GetTooltip() const { + return std::nullopt; +} + +void MenuItem::SetAccelerator(const std::optional& accelerator) { + ALOGW("MenuItem::SetAccelerator not implemented on Android"); +} + +KeyboardAccelerator MenuItem::GetAccelerator() const { + return KeyboardAccelerator(""); +} + +void MenuItem::SetEnabled(bool enabled) { + ALOGW("MenuItem::SetEnabled not implemented on Android"); +} + +bool MenuItem::IsEnabled() const { + return true; +} + +void MenuItem::SetState(MenuItemState state) { + ALOGW("MenuItem::SetState not implemented on Android"); +} + +MenuItemState MenuItem::GetState() const { + return MenuItemState::Unchecked; +} + +void MenuItem::SetRadioGroup(int group_id) { + ALOGW("MenuItem::SetRadioGroup not implemented on Android"); +} + +int MenuItem::GetRadioGroup() const { + return -1; +} + +void MenuItem::SetSubmenu(std::shared_ptr submenu) { + ALOGW("MenuItem::SetSubmenu not implemented on Android"); +} + +std::shared_ptr MenuItem::GetSubmenu() const { + return nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/message_dialog_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/message_dialog_android.cpp new file mode 100644 index 0000000..7ae0140 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/message_dialog_android.cpp @@ -0,0 +1,86 @@ +#include "../../dialog.h" +#include "../../message_dialog.h" + +namespace nativeapi { + +// Private implementation class for MessageDialog (Android stub) +class MessageDialog::Impl { + public: + Impl(const std::string& title, const std::string& message) : title_(title), message_(message) { + // TODO: Implement Android AlertDialog using JNI + // Should use android.app.AlertDialog.Builder + } + + ~Impl() { + // TODO: Cleanup if needed + } + + void SetTitle(const std::string& title) { title_ = title; } + + std::string GetTitle() const { return title_; } + + void SetMessage(const std::string& message) { message_ = message; } + + std::string GetMessage() const { return message_; } + + bool Open(DialogModality modality) { + // TODO: Implement using AlertDialog.Builder via JNI + // AlertDialog.Builder builder = new AlertDialog.Builder(context); + // builder.setTitle(title).setMessage(message).show(); + // For now, return false (not implemented) + return false; + } + + bool Close() { + // TODO: Implement closing logic using dialog.dismiss() + return false; + } + + private: + std::string title_; + std::string message_; +}; + +// MessageDialog implementation +MessageDialog::MessageDialog(const std::string& title, const std::string& message) + : pimpl_(std::make_unique(title, message)) { + // Set default modality to None (non-modal) + SetModality(DialogModality::None); +} + +MessageDialog::~MessageDialog() = default; + +void MessageDialog::SetTitle(const std::string& title) { + pimpl_->SetTitle(title); +} + +std::string MessageDialog::GetTitle() const { + return pimpl_->GetTitle(); +} + +void MessageDialog::SetMessage(const std::string& message) { + pimpl_->SetMessage(message); +} + +std::string MessageDialog::GetMessage() const { + return pimpl_->GetMessage(); +} + +DialogModality MessageDialog::GetModality() const { + return modality_; +} + +void MessageDialog::SetModality(DialogModality modality) { + modality_ = modality; +} + +bool MessageDialog::Open() { + DialogModality modality = GetModality(); + return pimpl_->Open(modality); +} + +bool MessageDialog::Close() { + return pimpl_->Close(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/preferences_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/preferences_android.cpp new file mode 100644 index 0000000..61e28e7 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/preferences_android.cpp @@ -0,0 +1,103 @@ +#include "../../preferences.h" + +namespace nativeapi { + +class Preferences::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + const std::string& GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +Preferences::Preferences() : Preferences("default") {} + +Preferences::Preferences(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +Preferences::~Preferences() = default; + +// Interface implementation +bool Preferences::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string Preferences::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool Preferences::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool Preferences::Clear() { + return pimpl_->Clear(); +} + +bool Preferences::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector Preferences::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t Preferences::GetSize() const { + return pimpl_->GetSize(); +} + +std::map Preferences::GetAll() const { + return pimpl_->GetAll(); +} + +std::string Preferences::GetScope() const { + return pimpl_->GetScope(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/secure_storage_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/secure_storage_android.cpp new file mode 100644 index 0000000..dffffc6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/secure_storage_android.cpp @@ -0,0 +1,107 @@ +#include "../../secure_storage.h" + +namespace nativeapi { + +class SecureStorage::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + std::string GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +SecureStorage::SecureStorage() : SecureStorage("default") {} + +SecureStorage::SecureStorage(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +SecureStorage::~SecureStorage() = default; + +bool SecureStorage::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string SecureStorage::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool SecureStorage::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool SecureStorage::Clear() { + return pimpl_->Clear(); +} + +bool SecureStorage::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector SecureStorage::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t SecureStorage::GetSize() const { + return pimpl_->GetSize(); +} + +std::map SecureStorage::GetAll() const { + return pimpl_->GetAll(); +} + +std::string SecureStorage::GetScope() const { + return pimpl_->GetScope(); +} + +bool SecureStorage::IsAvailable() { + // Stub implementation - report as unavailable + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/shortcut_manager_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/shortcut_manager_android.cpp new file mode 100644 index 0000000..3541859 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/shortcut_manager_android.cpp @@ -0,0 +1,27 @@ +#include "../../shortcut_manager.h" + +namespace nativeapi { + +class ShortcutManagerImpl final : public ShortcutManager::Impl { + public: + explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) {} + ~ShortcutManagerImpl() override = default; + + bool IsSupported() override { return false; } + bool RegisterShortcut(const std::shared_ptr& /*shortcut*/) override { return false; } + bool UnregisterShortcut(const std::shared_ptr& /*shortcut*/) override { return false; } + void SetupEventMonitoring() override {} + void CleanupEventMonitoring() override {} + + private: + ShortcutManager* manager_; +}; + +ShortcutManager::ShortcutManager() + : pimpl_(std::make_unique(this)), next_shortcut_id_(1), enabled_(true) {} + +ShortcutManager::~ShortcutManager() { + UnregisterAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/tray_icon_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/tray_icon_android.cpp new file mode 100644 index 0000000..6c79bab --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/tray_icon_android.cpp @@ -0,0 +1,97 @@ +#include +#include "../../tray_icon.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class TrayIcon::Impl { + public: + Impl() {} +}; + +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) {} +TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique()) {} +TrayIcon::~TrayIcon() {} + +void* TrayIcon::GetNativeObjectInternal() const { + return nullptr; +} + +TrayIconId TrayIcon::GetId() { + return IdAllocator::kInvalidId; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + ALOGW("TrayIcon::SetIcon uses Android notifications"); +} + +std::shared_ptr TrayIcon::GetIcon() const { + return nullptr; +} + +void TrayIcon::SetTitle(std::optional title) { + ALOGW("TrayIcon::SetTitle uses Android notification title"); +} + +std::optional TrayIcon::GetTitle() { + return std::nullopt; +} + +void TrayIcon::SetTooltip(std::optional tooltip) { + ALOGW("TrayIcon::SetTooltip uses Android notification content"); +} + +std::optional TrayIcon::GetTooltip() { + return std::nullopt; +} + +void TrayIcon::SetContextMenu(std::shared_ptr menu) { + ALOGW("TrayIcon::SetContextMenu uses Android notification actions"); +} + +std::shared_ptr TrayIcon::GetContextMenu() { + return nullptr; +} + +void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { + ALOGW("TrayIcon::SetContextMenuTrigger not applicable on Android"); +} + +ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { + return ContextMenuTrigger::None; +} + +Rectangle TrayIcon::GetBounds() { + return Rectangle{0.0, 0.0, 0.0, 0.0}; +} + +bool TrayIcon::SetVisible(bool visible) { + ALOGW("TrayIcon::SetVisible controls Android notification visibility"); + return true; +} + +bool TrayIcon::IsVisible() { + return false; +} + +bool TrayIcon::OpenContextMenu() { + ALOGW("TrayIcon::OpenContextMenu not applicable on Android"); + return false; +} + +void TrayIcon::StartEventListening() { + ALOGW("TrayIcon::StartEventListening not implemented on Android"); +} + +void TrayIcon::StopEventListening() { + ALOGW("TrayIcon::StopEventListening not implemented on Android"); +} + +bool TrayIcon::CloseContextMenu() { + ALOGW("TrayIcon::CloseContextMenu not applicable on Android"); + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/tray_manager_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/tray_manager_android.cpp new file mode 100644 index 0000000..84d73b7 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/tray_manager_android.cpp @@ -0,0 +1,39 @@ +#include +#include "../../tray_manager.h" + +#define LOG_TAG "NativeApi" +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +class TrayManager::Impl { + public: + Impl(TrayManager* manager) : manager_(manager) {} + TrayManager* manager_; +}; + +TrayManager::TrayManager() : pimpl_(std::make_unique(this)), next_tray_id_(1) {} +TrayManager::~TrayManager() {} + +bool TrayManager::IsSupported() { + ALOGW("TrayManager::IsSupported - uses Android notifications"); + return true; +} + +std::shared_ptr TrayManager::Get(TrayIconId id) { + std::lock_guard lock(mutex_); + auto it = trays_.find(id); + return (it != trays_.end()) ? it->second : nullptr; +} + +std::vector> TrayManager::GetAll() { + std::lock_guard lock(mutex_); + std::vector> result; + result.reserve(trays_.size()); + for (const auto& [id, tray] : trays_) { + result.push_back(tray); + } + return result; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/url_opener_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/url_opener_android.cpp new file mode 100644 index 0000000..7869510 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/url_opener_android.cpp @@ -0,0 +1,26 @@ +#include "../../url_opener.h" + +namespace nativeapi { +namespace { + +class AndroidUrlOpenerImpl final : public UrlOpener::Impl { + public: + bool IsSupported() const override { return false; } + + UrlOpenResult Open(const std::string& url) const override { + (void)url; + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kUnsupportedPlatform; + result.error_message = "URL opening is not implemented on Android in this native layer."; + return result; + } +}; + +} // namespace + +UrlOpener::UrlOpener() : pimpl_(std::make_unique()) {} + +UrlOpener::~UrlOpener() = default; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/window_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/window_android.cpp new file mode 100644 index 0000000..3dffeab --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/window_android.cpp @@ -0,0 +1,382 @@ +#include +#include +#include +#include "../../window.h" +#include "../../window_manager.h" + +#define LOG_TAG "NativeApi" +#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +// Private implementation class +class Window::Impl { + public: + Impl(ANativeWindow* window) : native_window_(window), visual_effect_(VisualEffect::None) {} + ANativeWindow* native_window_; + VisualEffect visual_effect_; +}; + +Window::Window() : pimpl_(std::make_unique(nullptr)) {} + +Window::Window(void* window) + : pimpl_(std::make_unique(static_cast(window))) {} + +Window::~Window() {} + +WindowId Window::GetId() const { + if (!pimpl_->native_window_) { + return IdAllocator::kInvalidId; + } + + // Store the allocated ID in a static map to ensure consistency + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + auto it = window_id_map.find(pimpl_->native_window_); + if (it != window_id_map.end()) { + return it->second; + } + + // Allocate new ID using the IdAllocator + WindowId new_id = IdAllocator::Allocate(); + if (new_id != IdAllocator::kInvalidId) { + window_id_map[pimpl_->native_window_] = new_id; + } + return new_id; +} + +void Window::Focus() { + if (pimpl_->native_window_) { + // On Android, focus is managed by the Activity lifecycle + // This is a no-op as the Activity manager handles focus + ALOGI("Window focus requested"); + } +} + +void Window::Blur() { + if (pimpl_->native_window_) { + // On Android, blur is managed by the Activity lifecycle + ALOGI("Window blur requested"); + } +} + +bool Window::IsFocused() const { + // Android manages focus through the Activity lifecycle + // We cannot reliably query focus state from NDK + return pimpl_->native_window_ != nullptr; +} + +void Window::Show() { + if (pimpl_->native_window_) { + // On Android, visibility is managed by the Activity lifecycle + // This would typically trigger onWindowShown callback in Java + ALOGI("Window show requested"); + } +} + +void Window::ShowInactive() { + if (pimpl_->native_window_) { + // Same as Show on Android + Show(); + } +} + +void Window::Hide() { + if (pimpl_->native_window_) { + // On Android, visibility is managed by the Activity lifecycle + ALOGI("Window hide requested"); + } +} + +bool Window::IsVisible() const { + return pimpl_->native_window_ != nullptr; +} + +void Window::Maximize() { + // Maximize is not applicable to Android Activities + ALOGW("Maximize not supported on Android"); +} + +void Window::Unmaximize() { + // Unmaximize is not applicable to Android Activities + ALOGW("Unmaximize not supported on Android"); +} + +bool Window::IsMaximized() const { + // Android Activities are typically fullscreen or windowed + return false; +} + +void Window::Minimize() { + // On Android, this would move the Activity to background + if (pimpl_->native_window_) { + ALOGI("Window minimize requested"); + } +} + +void Window::Restore() { + // On Android, restore would bring Activity to foreground + if (pimpl_->native_window_) { + ALOGI("Window restore requested"); + } +} + +bool Window::IsMinimized() const { + // Cannot reliably determine minimized state from NDK + return false; +} + +void Window::SetFullScreen(bool is_full_screen) { + // On Android, fullscreen is managed through Activity flags + if (pimpl_->native_window_) { + ALOGI("Fullscreen set to: %d", is_full_screen); + } +} + +bool Window::IsFullScreen() const { + // Cannot reliably determine fullscreen state from NDK + return false; +} + +void Window::SetBounds(Rectangle bounds) { + if (pimpl_->native_window_) { + ALOGI("SetBounds called: x=%f, y=%f, w=%f, h=%f", bounds.x, bounds.y, bounds.width, + bounds.height); + // Android windows resize is handled by the system + } +} + +Rectangle Window::GetBounds() const { + if (!pimpl_->native_window_) { + return Rectangle{0.0, 0.0, 0.0, 0.0}; + } + + // Get window dimensions from ANativeWindow + int32_t width = ANativeWindow_getWidth(pimpl_->native_window_); + int32_t height = ANativeWindow_getHeight(pimpl_->native_window_); + + return Rectangle{0.0, 0.0, static_cast(width), static_cast(height)}; +} + +void Window::SetSize(Size size, bool animate) { + if (pimpl_->native_window_) { + ALOGI("SetSize called: w=%f, h=%f", size.width, size.height); + // Size is managed by the Activity/View system + } +} + +Size Window::GetSize() const { + if (!pimpl_->native_window_) { + return Size{0.0, 0.0}; + } + + int32_t width = ANativeWindow_getWidth(pimpl_->native_window_); + int32_t height = ANativeWindow_getHeight(pimpl_->native_window_); + + return Size{static_cast(width), static_cast(height)}; +} + +void Window::SetContentSize(Size size) { + // On Android, content size is the same as window size + SetSize(size, false); +} + +Size Window::GetContentSize() const { + return GetSize(); +} + +void Window::SetContentBounds(Rectangle bounds) { + // On Android, content bounds is the same as window bounds + SetBounds(bounds); +} + +Rectangle Window::GetContentBounds() const { + // On Android, content bounds is the same as window bounds + return GetBounds(); +} + +void Window::SetMinimumSize(Size size) { + ALOGW("SetMinimumSize not fully supported on Android"); +} + +Size Window::GetMinimumSize() const { + return Size{0, 0}; +} + +void Window::SetMaximumSize(Size size) { + ALOGW("SetMaximumSize not fully supported on Android"); +} + +Size Window::GetMaximumSize() const { + return Size{0, 0}; +} + +void Window::SetResizable(bool is_resizable) { + ALOGW("SetResizable not supported on Android"); +} + +bool Window::IsResizable() const { + return false; +} + +void Window::SetMovable(bool is_movable) { + ALOGW("SetMovable not supported on Android"); +} + +bool Window::IsMovable() const { + return false; +} + +void Window::SetMinimizable(bool is_minimizable) { + ALOGW("SetMinimizable not supported on Android"); +} + +bool Window::IsMinimizable() const { + return true; +} + +void Window::SetMaximizable(bool is_maximizable) { + ALOGW("SetMaximizable not supported on Android"); +} + +bool Window::IsMaximizable() const { + return false; +} + +void Window::SetFullScreenable(bool is_full_screenable) { + ALOGW("SetFullScreenable not supported on Android"); +} + +bool Window::IsFullScreenable() const { + return true; +} + +void Window::SetClosable(bool is_closable) { + ALOGW("SetClosable not supported on Android"); +} + +bool Window::IsClosable() const { + return true; +} + +void Window::SetWindowControlButtonsVisible(bool is_visible) { + // Not applicable to Android - mobile apps don't have window control buttons +} + +bool Window::IsWindowControlButtonsVisible() const { + // Not applicable to Android - mobile apps don't have window control buttons + return false; +} + +void Window::SetAlwaysOnTop(bool is_always_on_top) { + ALOGW("SetAlwaysOnTop not fully supported on Android"); +} + +bool Window::IsAlwaysOnTop() const { + return false; +} + +void Window::SetPosition(Point point) { + ALOGW("SetPosition not supported on Android"); +} + +Point Window::GetPosition() const { + return Point{0, 0}; +} + +void Window::Center() { + // On Android, window positioning is not supported + // Activities are automatically managed by the system + ALOGW("Center not supported on Android - Activities are managed by the system"); +} + +void Window::SetTitle(std::string title) { + ALOGW("SetTitle not supported on Android (use Activity title)"); +} + +std::string Window::GetTitle() const { + return ""; +} + +void Window::SetTitleBarStyle(TitleBarStyle style) { + ALOGW("SetTitleBarStyle not supported on Android (use system UI visibility flags)"); +} + +TitleBarStyle Window::GetTitleBarStyle() const { + return TitleBarStyle::Normal; +} + +void Window::SetHasShadow(bool has_shadow) { + ALOGW("SetHasShadow not supported on Android"); +} + +bool Window::HasShadow() const { + return false; +} + +void Window::SetOpacity(float opacity) { + ALOGW("SetOpacity not supported on Android"); +} + +float Window::GetOpacity() const { + return 1.0f; +} + +void Window::SetVisualEffect(VisualEffect effect) { + pimpl_->visual_effect_ = effect; + ALOGW("SetVisualEffect not supported on Android"); +} + +VisualEffect Window::GetVisualEffect() const { + return pimpl_->visual_effect_; +} + +void Window::SetBackgroundColor(const Color& color) { + ALOGW("SetBackgroundColor not supported on Android"); +} + +Color Window::GetBackgroundColor() const { + return Color::White; +} + +void Window::SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces) { + ALOGW("SetVisibleOnAllWorkspaces not supported on Android"); +} + +bool Window::IsVisibleOnAllWorkspaces() const { + return false; +} + +void Window::SetIgnoreMouseEvents(bool is_ignore_mouse_events) { + ALOGW("SetIgnoreMouseEvents not supported on Android"); +} + +bool Window::IsIgnoreMouseEvents() const { + return false; +} + +void Window::SetFocusable(bool is_focusable) { + ALOGW("SetFocusable not supported on Android"); +} + +bool Window::IsFocusable() const { + return true; +} + +void Window::StartDragging() { + ALOGW("StartDragging not supported on Android"); +} + +void Window::StartResizing() { + ALOGW("StartResizing not supported on Android"); +} + +void* Window::GetNativeObjectInternal() const { + return static_cast(pimpl_->native_window_); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/android/window_manager_android.cpp b/packages/cnativeapi/cxx_impl/src/platform/android/window_manager_android.cpp new file mode 100644 index 0000000..1f6ab4a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/android/window_manager_android.cpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" + +#define LOG_TAG "NativeApi" +#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + +namespace nativeapi { + +// Helper function to manage mapping between ANativeWindow pointers and WindowIds +static WindowId GetOrCreateWindowId(ANativeWindow* native_window) { + if (!native_window) { + return IdAllocator::kInvalidId; + } + + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + auto it = window_id_map.find(native_window); + if (it != window_id_map.end()) { + return it->second; + } + + // Allocate new ID using the IdAllocator + WindowId new_id = IdAllocator::Allocate(); + if (new_id != IdAllocator::kInvalidId) { + window_id_map[native_window] = new_id; + } + return new_id; +} + +// Helper function to find ANativeWindow by WindowId +static ANativeWindow* FindNativeWindowById(WindowId id) { + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + for (const auto& pair : window_id_map) { + if (pair.second == id) { + return pair.first; + } + } + return nullptr; +} + +// Private implementation for Android +class WindowManager::Impl { + public: + Impl(WindowManager* manager) : manager_(manager) {} + ~Impl() {} + + void StartEventListening() { + // On Android, event monitoring is done through Activity callbacks + // Setup will be handled by the Activity lifecycle + ALOGI("Window event monitoring setup"); + } + + void StopEventListening() { ALOGI("Window event monitoring cleanup"); } + + private: + WindowManager* manager_; +}; + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + StartEventListening(); +} + +WindowManager::~WindowManager() { + StopEventListening(); +} + +std::shared_ptr WindowManager::Get(WindowId id) { + auto cached = WindowRegistry::GetInstance().Get(id); + if (cached) { + return cached; + } + + // Try to find the window by ID + ANativeWindow* native_window = FindNativeWindowById(id); + if (native_window) { + auto window = std::make_shared(static_cast(native_window)); + WindowRegistry::GetInstance().Add(id, window); + return window; + } + + return nullptr; +} + +std::vector> WindowManager::GetAll() { + return WindowRegistry::GetInstance().GetAll(); +} + +std::shared_ptr WindowManager::GetCurrent() { + // On Android, the current window is typically the Activity's native window + // This would need to be set by the Activity lifecycle callbacks + auto all = WindowRegistry::GetInstance().GetAll(); + return all.empty() ? nullptr : all.front(); +} + +void WindowManager::SetWillShowHook(std::optional hook) { + // Empty implementation +} + +void WindowManager::SetWillHideHook(std::optional hook) { + // Empty implementation +} + +bool WindowManager::HasWillShowHook() const { + return false; +} + +bool WindowManager::HasWillHideHook() const { + return false; +} + +void WindowManager::HandleWillShow(WindowId id) { + // Empty implementation +} + +void WindowManager::HandleWillHide(WindowId id) { + // Empty implementation +} + +bool WindowManager::CallOriginalShow(WindowId id) { + // Android doesn't support swizzling for window show/hide + // Return false to indicate unsupported + return false; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + // Android doesn't support swizzling for window show/hide + // Return false to indicate unsupported + return false; +} + +void WindowManager::StartEventListening() { + pimpl_->StartEventListening(); +} + +void WindowManager::StopEventListening() { + pimpl_->StopEventListening(); +} + +void WindowManager::DispatchWindowEvent(const WindowEvent& event) { + Emit(event); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/accessibility_manager_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/accessibility_manager_ios.mm new file mode 100644 index 0000000..08aa597 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/accessibility_manager_ios.mm @@ -0,0 +1,18 @@ +#import +#import +#include "../../accessibility_manager.h" + +namespace nativeapi { + +void AccessibilityManager::Enable() { + // On iOS, accessibility features are controlled by system settings + // This method sets the internal flag + enabled_ = true; +} + +bool AccessibilityManager::IsEnabled() { + // Return the internal enabled state + return enabled_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/application_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/application_ios.mm new file mode 100644 index 0000000..9044d94 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/application_ios.mm @@ -0,0 +1,70 @@ +#import +#import +#include +#include "../../application.h" +#include "../../window_manager.h" + +namespace nativeapi { + +class Application::Impl { + public: + Impl() {} +}; + +Application::Application() : pimpl_(std::make_unique()) {} +Application::~Application() {} + +int Application::Run() { + // On iOS, application lifecycle is managed by UIApplication + // This is typically handled in the AppDelegate + return 0; +} + +int Application::Run(std::shared_ptr window) { + // iOS manages app lifecycle through UIApplication + return 0; +} + +void Application::Quit(int exit_code) { + // On iOS, apps don't exit programmatically + // The system manages app lifecycle +} + +bool Application::IsRunning() const { + UIApplication* app = [UIApplication sharedApplication]; + return app != nil; +} + +bool Application::IsSingleInstance() const { + return true; // iOS apps are always single instance +} + +bool Application::SetIcon(const std::string& icon_path) { + // iOS app icons are set in Info.plist + return false; +} + +bool Application::SetDockIconVisible(bool visible) { + // Not applicable to iOS + return false; +} + +bool Application::SetMenuBar(std::shared_ptr menu) { + // iOS doesn't have a menu bar + return false; +} + +std::shared_ptr Application::GetPrimaryWindow() const { + return nullptr; +} + +void Application::SetPrimaryWindow(std::shared_ptr window) { + // iOS manages primary window through UIApplication +} + +std::vector> Application::GetAllWindows() const { + auto& window_manager = WindowManager::GetInstance(); + return window_manager.GetAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/dispatcher_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/dispatcher_ios.mm new file mode 100644 index 0000000..07d20cf --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/dispatcher_ios.mm @@ -0,0 +1,45 @@ +#include "../../foundation/dispatcher_platform.h" + +#import +#import +#include + +namespace nativeapi { +namespace dispatcher_platform { + +bool PlatformIsMainThread() { + return [NSThread isMainThread]; +} + +void PlatformSetMainThread() { + // No-op: on Apple platforms the OS is authoritative about which thread is the + // main thread, so there is nothing for the caller to correct. +} + +bool PlatformIsMainThreadDispatchSupported() { + return true; +} + +bool PlatformRunOnMainThread(std::function fn) { + if (!fn) { + return true; + } + + // Heap-allocate rather than capturing the std::function in a __block variable: + // block capture of non-trivial C++ types differs between ARC and non-ARC + // translation units, and this file is compiled into both configurations. + auto* work = new std::function(std::move(fn)); + dispatch_async(dispatch_get_main_queue(), ^{ + (*work)(); + delete work; + }); + return true; +} + +bool PlatformRunMainThreadLoopFor(int timeout_ms) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout_ms / 1000.0, false); + return true; +} + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/display_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/display_ios.mm new file mode 100644 index 0000000..82527f8 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/display_ios.mm @@ -0,0 +1,107 @@ +#import +#import +#include +#include "../../display.h" + +namespace nativeapi { + +// Private implementation class +class Display::Impl { + public: + Impl(UIScreen* screen) : ui_screen_(screen) {} + + const DisplayId id_ = IdAllocator::Allocate(); + UIScreen* ui_screen_; +}; + +Display::Display(void* display) : pimpl_(std::make_unique((__bridge UIScreen*)display)) {} + +Display::~Display() {} + +DisplayId Display::GetId() const { + return pimpl_->id_; +} + +std::string Display::GetName() const { + if (!pimpl_->ui_screen_) { + return "Unknown"; + } + + // iOS doesn't provide a friendly name for screens + // Use scale factor and size to differentiate + CGFloat scale = pimpl_->ui_screen_.scale; + CGRect bounds = pimpl_->ui_screen_.bounds; + + return std::string("Screen ") + std::to_string((int)bounds.size.width) + "x" + + std::to_string((int)bounds.size.height) + "@" + std::to_string((int)scale) + "x"; +} + +Point Display::GetPosition() const { + if (!pimpl_->ui_screen_) { + return Point{0, 0}; + } + + CGRect bounds = pimpl_->ui_screen_.bounds; + return Point{static_cast(bounds.origin.x), static_cast(bounds.origin.y)}; +} + +Size Display::GetSize() const { + if (!pimpl_->ui_screen_) { + return Size{0, 0}; + } + + CGRect bounds = pimpl_->ui_screen_.bounds; + return Size{static_cast(bounds.size.width), static_cast(bounds.size.height)}; +} + +Rectangle Display::GetWorkArea() const { + if (!pimpl_->ui_screen_) { + return Rectangle{0, 0, 0, 0}; + } + + CGRect bounds = pimpl_->ui_screen_.bounds; + return Rectangle{static_cast(bounds.origin.x), static_cast(bounds.origin.y), + static_cast(bounds.size.width), static_cast(bounds.size.height)}; +} + +double Display::GetScaleFactor() const { + return pimpl_->ui_screen_ ? static_cast(pimpl_->ui_screen_.scale) : 1.0; +} + +bool Display::IsPrimary() const { + return pimpl_->ui_screen_ == [UIScreen mainScreen]; +} + +DisplayOrientation Display::GetOrientation() const { + if (!pimpl_->ui_screen_) { + return DisplayOrientation::kPortrait; + } + + // Check orientation based on screen bounds + CGRect bounds = pimpl_->ui_screen_.bounds; + if (bounds.size.width > bounds.size.height) { + return DisplayOrientation::kLandscape; + } else { + return DisplayOrientation::kPortrait; + } +} + +int Display::GetRefreshRate() const { + if (!pimpl_->ui_screen_) { + return 60; + } + + // iOS doesn't expose refresh rate directly, return standard 60Hz + return 60; +} + +int Display::GetBitDepth() const { + // iOS devices typically use 32-bit color depth + return 32; +} + +void* Display::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ui_screen_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/display_manager_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/display_manager_ios.mm new file mode 100644 index 0000000..3a47543 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/display_manager_ios.mm @@ -0,0 +1,43 @@ +#import +#import +#include +#include +#include "../../display_manager.h" + +namespace nativeapi { + +DisplayManager::DisplayManager() { + // Prime the instance cache so the first change notification diffs against + // the displays present at startup. + GetAll(); +} + +DisplayManager::~DisplayManager() {} + +std::vector DisplayManager::EnumerateNativeDisplays() { + std::vector natives; + + UIScreen* mainScreen = [UIScreen mainScreen]; + NSArray* screens = [UIScreen screens]; + for (UIScreen* screen in screens) { + // A UIScreen object is stable for as long as the screen stays connected, + // so its address serves as the identity key. + natives.push_back({std::to_string(reinterpret_cast((__bridge void*)screen)), + (__bridge void*)screen, screen == mainScreen}); + } + + // If no screens found, add main screen + if (natives.empty() && mainScreen) { + natives.push_back({std::to_string(reinterpret_cast((__bridge void*)mainScreen)), + (__bridge void*)mainScreen, true}); + } + + return natives; +} + +Point DisplayManager::GetCursorPosition() { + // iOS doesn't have a cursor position concept + return Point{0, 0}; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/image_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/image_ios.mm new file mode 100644 index 0000000..bf7d34f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/image_ios.mm @@ -0,0 +1,123 @@ +#import +#import +#include +#include +#include "../../image.h" + +namespace nativeapi { + +class Image::Impl { + public: + Impl() : ui_image_(nil), size_({0, 0}), format_("Unknown") {} + + UIImage* ui_image_; + Size size_; + std::string format_; +}; + +Image::Image() : pimpl_(std::make_unique()) {} +Image::~Image() {} + +Image::Image(const Image& other) : pimpl_(std::make_unique()) { + if (other.pimpl_ && other.pimpl_->ui_image_) { + pimpl_->ui_image_ = other.pimpl_->ui_image_; + pimpl_->size_ = other.pimpl_->size_; + pimpl_->format_ = other.pimpl_->format_; + } +} + +Image::Image(Image&& other) noexcept : pimpl_(std::move(other.pimpl_)) {} + +std::shared_ptr Image::FromFile(const std::string& file_path) { + auto image = std::shared_ptr(new Image()); + NSString* nsPath = [NSString stringWithUTF8String:file_path.c_str()]; + UIImage* uiImage = [UIImage imageWithContentsOfFile:nsPath]; + + if (uiImage) { + image->pimpl_->ui_image_ = uiImage; + + // Get actual image size + CGSize size = uiImage.size; + image->pimpl_->size_ = {static_cast(size.width), static_cast(size.height)}; + + // Determine format from file extension + NSString* extension = [[nsPath pathExtension] lowercaseString]; + if ([extension isEqualToString:@"png"]) { + image->pimpl_->format_ = "PNG"; + } else if ([extension isEqualToString:@"jpg"] || [extension isEqualToString:@"jpeg"]) { + image->pimpl_->format_ = "JPEG"; + } else if ([extension isEqualToString:@"gif"]) { + image->pimpl_->format_ = "GIF"; + } else { + image->pimpl_->format_ = "Unknown"; + } + } else { + return nullptr; + } + + return image; +} + +std::shared_ptr Image::FromBase64(const std::string& base64_data) { + auto image = std::shared_ptr(new Image()); + // Parse data URI if present + std::string data = base64_data; + size_t pos = data.find(","); + if (pos != std::string::npos) { + data = data.substr(pos + 1); + } + + NSData* nsData = + [[NSData alloc] initWithBase64EncodedString:[NSString stringWithUTF8String:data.c_str()] + options:0]; + UIImage* uiImage = [UIImage imageWithData:nsData]; + + if (uiImage) { + image->pimpl_->ui_image_ = uiImage; + + // Get actual image size + CGSize size = uiImage.size; + image->pimpl_->size_ = {static_cast(size.width), static_cast(size.height)}; + + // Default assumption for base64 images + image->pimpl_->format_ = "PNG"; + } else { + return nullptr; + } + + return image; +} + +Size Image::GetSize() const { + return pimpl_->size_; +} + +std::string Image::GetFormat() const { + return pimpl_->format_; +} + +std::string Image::ToBase64() const { + if (!pimpl_->ui_image_) { + return ""; + } + + NSData* pngData = UIImagePNGRepresentation(pimpl_->ui_image_); + NSString* base64String = [pngData base64EncodedStringWithOptions:0]; + return std::string("data:image/png;base64,") + [base64String UTF8String]; +} + +bool Image::SaveToFile(const std::string& file_path) const { + if (!pimpl_->ui_image_) { + return false; + } + + NSData* pngData = UIImagePNGRepresentation(pimpl_->ui_image_); + NSString* nsPath = [NSString stringWithUTF8String:file_path.c_str()]; + return [pngData writeToFile:nsPath atomically:YES]; +} + +void* Image::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ui_image_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/keyboard_monitor_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/keyboard_monitor_ios.mm new file mode 100644 index 0000000..0924a7b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/keyboard_monitor_ios.mm @@ -0,0 +1,28 @@ +#import +#import +#include "../../keyboard_monitor.h" + +namespace nativeapi { + +class KeyboardMonitor::Impl { + public: + Impl() {} +}; + +KeyboardMonitor::KeyboardMonitor() : impl_(std::make_unique()) {} +KeyboardMonitor::~KeyboardMonitor() {} + +void KeyboardMonitor::Start() { + // iOS keyboard monitoring requires special permissions + // Implementation would need accessibility services +} + +void KeyboardMonitor::Stop() { + // Stop monitoring +} + +bool KeyboardMonitor::IsMonitoring() const { + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/launch_at_login_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/launch_at_login_ios.mm new file mode 100644 index 0000000..55d0419 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/launch_at_login_ios.mm @@ -0,0 +1,107 @@ +#import + +#include "../../launch_at_login.h" + +namespace nativeapi { + +/** + * iOS stub implementation for LaunchAtLogin. + * + * Auto-start at user login is not supported on iOS. All operations that would + * enable/disable or configure launch-at-login return false. Getters return the + * locally stored values (typically empty), while setters return false and do + * not modify state. + */ +class LaunchAtLogin::Impl { + public: + // Unsupported platform semantics + static bool IsSupported() { return false; } + + Impl() = default; + + explicit Impl(const std::string& id) : id_(id) {} + + Impl(const std::string& id, const std::string& display_name) + : id_(id), display_name_(display_name) {} + + ~Impl() = default; + + // Getters return whatever is locally available (likely empty) + std::string GetId() const { return id_; } + std::string GetDisplayName() const { return display_name_; } + + // No-op setter; returns false to indicate unsupported + bool SetDisplayName(const std::string& /*display_name*/) { return false; } + + bool SetProgram(const std::string& /*executable_path*/, + const std::vector& /*arguments*/) { + return false; + } + + std::string GetExecutablePath() const { return program_path_; } + std::vector GetArguments() const { return arguments_; } + + bool Enable() { return false; } + bool Disable() { return false; } + bool IsEnabled() const { return false; } + + private: + std::string id_; + std::string display_name_; + std::string program_path_; + std::vector arguments_; +}; + +// LaunchAtLogin public API forwarding to Impl + +LaunchAtLogin::LaunchAtLogin() : pimpl_(std::make_unique()) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id) : pimpl_(std::make_unique(id)) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id, const std::string& display_name) + : pimpl_(std::make_unique(id, display_name)) {} + +LaunchAtLogin::~LaunchAtLogin() = default; + +bool LaunchAtLogin::IsSupported() { + return Impl::IsSupported(); +} + +std::string LaunchAtLogin::GetId() const { + return pimpl_->GetId(); +} + +std::string LaunchAtLogin::GetDisplayName() const { + return pimpl_->GetDisplayName(); +} + +bool LaunchAtLogin::SetDisplayName(const std::string& display_name) { + return pimpl_->SetDisplayName(display_name); +} + +bool LaunchAtLogin::SetProgram(const std::string& executable_path, + const std::vector& arguments) { + return pimpl_->SetProgram(executable_path, arguments); +} + +std::string LaunchAtLogin::GetExecutablePath() const { + return pimpl_->GetExecutablePath(); +} + +std::vector LaunchAtLogin::GetArguments() const { + return pimpl_->GetArguments(); +} + +bool LaunchAtLogin::Enable() { + return pimpl_->Enable(); +} + +bool LaunchAtLogin::Disable() { + return pimpl_->Disable(); +} + +bool LaunchAtLogin::IsEnabled() const { + return pimpl_->IsEnabled(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/menu_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/menu_ios.mm new file mode 100644 index 0000000..14c4679 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/menu_ios.mm @@ -0,0 +1,210 @@ +#import +#import +#include "../../foundation/id_allocator.h" +#include "../../image.h" +#include "../../menu.h" + +namespace nativeapi { + +// MenuItem::Impl implementation +class MenuItem::Impl { + public: + MenuItemId id_; + MenuItemType type_; + std::optional label_; + std::shared_ptr image_; + std::optional tooltip_; + KeyboardAccelerator accelerator_; + bool has_accelerator_; + MenuItemState state_; + int radio_group_; + std::shared_ptr submenu_; + + Impl(MenuItemId id, MenuItemType type) + : id_(id), + type_(type), + accelerator_("", ModifierKey::None), + has_accelerator_(false), + state_(MenuItemState::Unchecked), + radio_group_(-1) {} +}; + +// MenuItem implementation +MenuItem::MenuItem(const std::string& label, MenuItemType type) { + MenuItemId id = IdAllocator::Allocate(); + pimpl_ = std::make_unique(id, type); + pimpl_->label_ = label.empty() ? std::nullopt : std::optional(label); +} + +MenuItem::MenuItem(void* native_item) { + MenuItemId id = IdAllocator::Allocate(); + pimpl_ = std::make_unique(id, MenuItemType::Normal); +} + +MenuItem::~MenuItem() {} + +MenuItemId MenuItem::GetId() const { + return pimpl_->id_; +} + +MenuItemType MenuItem::GetType() const { + return pimpl_->type_; +} + +void MenuItem::SetLabel(const std::optional& label) { + pimpl_->label_ = label; +} + +std::optional MenuItem::GetLabel() const { + return pimpl_->label_; +} + +void MenuItem::SetIcon(std::shared_ptr image) { + pimpl_->image_ = image; +} + +std::shared_ptr MenuItem::GetIcon() const { + return pimpl_->image_; +} + +void MenuItem::SetTooltip(const std::optional& tooltip) { + pimpl_->tooltip_ = tooltip; +} + +std::optional MenuItem::GetTooltip() const { + return pimpl_->tooltip_; +} + +void MenuItem::SetAccelerator(const std::optional& accelerator) { + if (accelerator.has_value()) { + pimpl_->accelerator_ = *accelerator; + pimpl_->has_accelerator_ = true; + } else { + pimpl_->has_accelerator_ = false; + pimpl_->accelerator_ = KeyboardAccelerator("", ModifierKey::None); + } +} + +KeyboardAccelerator MenuItem::GetAccelerator() const { + if (pimpl_->has_accelerator_) { + return pimpl_->accelerator_; + } + return KeyboardAccelerator("", ModifierKey::None); +} + +void MenuItem::SetEnabled(bool enabled) { + // iOS implementation would go here +} + +bool MenuItem::IsEnabled() const { + return true; +} + +void MenuItem::SetState(MenuItemState state) { + if (pimpl_->type_ == MenuItemType::Checkbox || pimpl_->type_ == MenuItemType::Radio) { + if (pimpl_->type_ == MenuItemType::Radio && state == MenuItemState::Mixed) { + return; + } + pimpl_->state_ = state; + } +} + +MenuItemState MenuItem::GetState() const { + return pimpl_->state_; +} + +void MenuItem::SetRadioGroup(int group_id) { + pimpl_->radio_group_ = group_id; +} + +int MenuItem::GetRadioGroup() const { + return pimpl_->radio_group_; +} + +void MenuItem::SetSubmenu(std::shared_ptr submenu) { + pimpl_->submenu_ = submenu; +} + +std::shared_ptr MenuItem::GetSubmenu() const { + return pimpl_->submenu_; +} + +void* MenuItem::GetNativeObjectInternal() const { + return nullptr; +} + +// Menu::Impl implementation +class Menu::Impl { + public: + Impl() {} +}; + +Menu::Menu() : pimpl_(std::make_unique()) {} +Menu::Menu(void* native_menu) : pimpl_(std::make_unique()) {} +Menu::~Menu() {} + +MenuId Menu::GetId() const { + return IdAllocator::kInvalidId; +} + +void Menu::AddItem(std::shared_ptr item) { + // iOS menus are context menus or action sheets +} + +void Menu::InsertItem(size_t index, std::shared_ptr item) { + // Implementation needed +} + +bool Menu::RemoveItem(std::shared_ptr item) { + return false; +} + +bool Menu::RemoveItemById(MenuItemId item_id) { + return false; +} + +bool Menu::RemoveItemAt(size_t index) { + return false; +} + +void Menu::Clear() { + // Implementation needed +} + +void Menu::AddSeparator() { + // Implementation needed +} + +void Menu::InsertSeparator(size_t index) { + // Implementation needed +} + +size_t Menu::GetItemCount() const { + return 0; +} + +std::shared_ptr Menu::GetItemAt(size_t index) const { + return nullptr; +} + +std::shared_ptr Menu::GetItemById(MenuItemId item_id) const { + return nullptr; +} + +std::vector> Menu::GetAllItems() const { + return std::vector>(); +} + +bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { + return false; +} + +bool Menu::Close() { + return false; +} + +void* Menu::GetNativeObjectInternal() const { + return nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/message_dialog_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/message_dialog_ios.mm new file mode 100644 index 0000000..aa79b41 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/message_dialog_ios.mm @@ -0,0 +1,94 @@ +#import +#include "../../dialog.h" +#include "../../message_dialog.h" + +namespace nativeapi { + +// Private implementation class for MessageDialog (iOS stub) +class MessageDialog::Impl { + public: + Impl(const std::string& title, const std::string& message) + : title_(title), message_(message), alert_controller_(nil) { + // TODO: Implement iOS UIAlertController + // Should use UIAlertController with UIAlertControllerStyleAlert + } + + ~Impl() { + if (alert_controller_) { + alert_controller_ = nil; + } + } + + void SetTitle(const std::string& title) { title_ = title; } + + std::string GetTitle() const { return title_; } + + void SetMessage(const std::string& message) { message_ = message; } + + std::string GetMessage() const { return message_; } + + bool Open(DialogModality modality) { + // TODO: Implement using UIAlertController + // UIAlertController *alert = [UIAlertController + // alertControllerWithTitle:@"title" + // message:@"message" + // preferredStyle:UIAlertControllerStyleAlert]; + // [viewController presentViewController:alert animated:YES completion:nil]; + // For now, return false (not implemented) + return false; + } + + bool Close() { + // TODO: Implement closing logic using dismissViewControllerAnimated + return false; + } + + private: + std::string title_; + std::string message_; + UIAlertController* alert_controller_; +}; + +// MessageDialog implementation +MessageDialog::MessageDialog(const std::string& title, const std::string& message) + : pimpl_(std::make_unique(title, message)) { + // Set default modality to None (non-modal) + SetModality(DialogModality::None); +} + +MessageDialog::~MessageDialog() = default; + +void MessageDialog::SetTitle(const std::string& title) { + pimpl_->SetTitle(title); +} + +std::string MessageDialog::GetTitle() const { + return pimpl_->GetTitle(); +} + +void MessageDialog::SetMessage(const std::string& message) { + pimpl_->SetMessage(message); +} + +std::string MessageDialog::GetMessage() const { + return pimpl_->GetMessage(); +} + +DialogModality MessageDialog::GetModality() const { + return modality_; +} + +void MessageDialog::SetModality(DialogModality modality) { + modality_ = modality; +} + +bool MessageDialog::Open() { + DialogModality modality = GetModality(); + return pimpl_->Open(modality); +} + +bool MessageDialog::Close() { + return pimpl_->Close(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/preferences_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/preferences_ios.mm new file mode 100644 index 0000000..52a0825 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/preferences_ios.mm @@ -0,0 +1,104 @@ +#import +#include "../../preferences.h" + +namespace nativeapi { + +class Preferences::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + const std::string& GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +Preferences::Preferences() : Preferences("default") {} + +Preferences::Preferences(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +Preferences::~Preferences() = default; + +// Interface implementation +bool Preferences::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string Preferences::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool Preferences::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool Preferences::Clear() { + return pimpl_->Clear(); +} + +bool Preferences::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector Preferences::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t Preferences::GetSize() const { + return pimpl_->GetSize(); +} + +std::map Preferences::GetAll() const { + return pimpl_->GetAll(); +} + +std::string Preferences::GetScope() const { + return pimpl_->GetScope(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/secure_storage_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/secure_storage_ios.mm new file mode 100644 index 0000000..9077089 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/secure_storage_ios.mm @@ -0,0 +1,108 @@ +#import +#include "../../secure_storage.h" + +namespace nativeapi { + +class SecureStorage::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + std::string GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +SecureStorage::SecureStorage() : SecureStorage("default") {} + +SecureStorage::SecureStorage(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +SecureStorage::~SecureStorage() = default; + +bool SecureStorage::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string SecureStorage::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool SecureStorage::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool SecureStorage::Clear() { + return pimpl_->Clear(); +} + +bool SecureStorage::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector SecureStorage::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t SecureStorage::GetSize() const { + return pimpl_->GetSize(); +} + +std::map SecureStorage::GetAll() const { + return pimpl_->GetAll(); +} + +std::string SecureStorage::GetScope() const { + return pimpl_->GetScope(); +} + +bool SecureStorage::IsAvailable() { + // Stub implementation - report as unavailable + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/shortcut_manager_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/shortcut_manager_ios.mm new file mode 100644 index 0000000..3541859 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/shortcut_manager_ios.mm @@ -0,0 +1,27 @@ +#include "../../shortcut_manager.h" + +namespace nativeapi { + +class ShortcutManagerImpl final : public ShortcutManager::Impl { + public: + explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) {} + ~ShortcutManagerImpl() override = default; + + bool IsSupported() override { return false; } + bool RegisterShortcut(const std::shared_ptr& /*shortcut*/) override { return false; } + bool UnregisterShortcut(const std::shared_ptr& /*shortcut*/) override { return false; } + void SetupEventMonitoring() override {} + void CleanupEventMonitoring() override {} + + private: + ShortcutManager* manager_; +}; + +ShortcutManager::ShortcutManager() + : pimpl_(std::make_unique(this)), next_shortcut_id_(1), enabled_(true) {} + +ShortcutManager::~ShortcutManager() { + UnregisterAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/tray_icon_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/tray_icon_ios.mm new file mode 100644 index 0000000..abe181c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/tray_icon_ios.mm @@ -0,0 +1,93 @@ +#import +#import +#include +#include "../../tray_icon.h" + +namespace nativeapi { + +class TrayIcon::Impl { + public: + Impl() {} +}; + +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) {} +TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique()) {} +TrayIcon::~TrayIcon() {} + +TrayIconId TrayIcon::GetId() { + return IdAllocator::kInvalidId; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + // iOS doesn't have system tray icons +} + +std::shared_ptr TrayIcon::GetIcon() const { + return nullptr; +} + +void TrayIcon::SetTitle(std::optional title) { + // Not applicable to iOS +} + +std::optional TrayIcon::GetTitle() { + return std::nullopt; +} + +void TrayIcon::SetTooltip(std::optional tooltip) { + // Not applicable to iOS +} + +std::optional TrayIcon::GetTooltip() { + return std::nullopt; +} + +void TrayIcon::SetContextMenu(std::shared_ptr menu) { + // Not applicable to iOS +} + +std::shared_ptr TrayIcon::GetContextMenu() { + return nullptr; +} + +void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { + // Not applicable to iOS +} + +ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { + return ContextMenuTrigger::None; +} + +Rectangle TrayIcon::GetBounds() { + return Rectangle{0, 0, 0, 0}; +} + +bool TrayIcon::SetVisible(bool visible) { + return false; +} + +bool TrayIcon::IsVisible() { + return false; +} + +bool TrayIcon::OpenContextMenu() { + return false; +} + +bool TrayIcon::CloseContextMenu() { + return false; +} + +void TrayIcon::StartEventListening() { + // No event monitoring needed on iOS +} + +void TrayIcon::StopEventListening() { + // No cleanup needed +} + +void* TrayIcon::GetNativeObjectInternal() const { + return nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/tray_manager_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/tray_manager_ios.mm new file mode 100644 index 0000000..a64a498 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/tray_manager_ios.mm @@ -0,0 +1,39 @@ +#import +#import +#include "../../tray_manager.h" + +namespace nativeapi { + +class TrayManager::Impl { + public: + Impl(TrayManager* manager) : manager_(manager) {} + TrayManager* manager_; +}; + +TrayManager::TrayManager() : pimpl_(std::make_unique(this)), next_tray_id_(1) {} +TrayManager::~TrayManager() {} + +bool TrayManager::IsSupported() { + // iOS doesn't support system tray icons + return false; +} + +std::shared_ptr TrayManager::Get(TrayIconId id) { + std::lock_guard lock(mutex_); + auto it = trays_.find(id); + return (it != trays_.end()) ? it->second : nullptr; +} + +std::vector> TrayManager::GetAll() { + std::lock_guard lock(mutex_); + std::vector> result; + result.reserve(trays_.size()); + + for (const auto& [id, tray] : trays_) { + result.push_back(tray); + } + + return result; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/url_opener_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/url_opener_ios.mm new file mode 100644 index 0000000..9d6ae43 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/url_opener_ios.mm @@ -0,0 +1,140 @@ +#import +#import + +#include + +#include "../../url_opener.h" + +namespace nativeapi { +namespace { + +UrlOpenResult LaunchUrlOnMainThread(const std::string& url) { + @autoreleasepool { + UIApplication* app = [UIApplication sharedApplication]; + if (!app) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "UIApplication is unavailable."; + return result; + } + + NSString* ns_url = [NSString stringWithUTF8String:url.c_str()]; + if (!ns_url) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "Failed to build NSURL from UTF-8 input."; + return result; + } + + NSURL* target = [NSURL URLWithString:ns_url]; + if (!target) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "Failed to parse URL."; + return result; + } + + if (![app canOpenURL:target]) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "No handler available for URL."; + return result; + } + + if (@available(iOS 10.0, *)) { + __block BOOL open_result = NO; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [app openURL:target + options:@{} +completionHandler:^(BOOL success) { + open_result = success; + dispatch_semaphore_signal(semaphore); +}]; + + const long wait_result = + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC)); + if (wait_result != 0) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "Timed out waiting for openURL completion."; + return result; + } + if (!open_result) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "UIApplication failed to open URL."; + return result; + } + UrlOpenResult result; + result.success = true; + result.error_code = UrlOpenErrorCode::kNone; + return result; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + const BOOL opened = [app openURL:target]; +#pragma clang diagnostic pop + if (!opened) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "UIApplication failed to open URL."; + return result; + } + UrlOpenResult result; + result.success = true; + result.error_code = UrlOpenErrorCode::kNone; + return result; + } +} + +UrlOpenResult LaunchUrl(const std::string& url) { + if ([NSThread isMainThread]) { + return LaunchUrlOnMainThread(url); + } + + __block UrlOpenResult outcome; + dispatch_sync(dispatch_get_main_queue(), ^{ + outcome = LaunchUrlOnMainThread(url); + }); + return outcome; +} + +class IosUrlOpenerImpl final : public UrlOpener::Impl { + public: + bool IsSupported() const override { return true; } + + bool CanOpen(const std::string& url) const override { + @autoreleasepool { + NSString* ns_url = [NSString stringWithUTF8String:url.c_str()]; + if (!ns_url) return false; + + NSURL* target = [NSURL URLWithString:ns_url]; + if (!target) return false; + + UIApplication* app = [UIApplication sharedApplication]; + if (!app) return false; + + return [app canOpenURL:target]; + } + } + + UrlOpenResult Open(const std::string& url) const override { + return LaunchUrl(url); + } +}; + +} // namespace + +UrlOpener::UrlOpener() : pimpl_(std::make_unique()) {} + +UrlOpener::~UrlOpener() = default; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/window_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/window_ios.mm new file mode 100644 index 0000000..ac94ed3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/window_ios.mm @@ -0,0 +1,425 @@ +#import +#import +#include +#include "../../window.h" +#include "../../window_manager.h" + +namespace nativeapi { + +// Private implementation class +class Window::Impl { + public: + Impl(UIWindow* window) : ui_window_(window), visual_effect_(VisualEffect::None) {} + UIWindow* ui_window_; + VisualEffect visual_effect_; +}; + +Window::Window() : pimpl_(std::make_unique(nil)) {} + +Window::Window(void* window) : pimpl_(std::make_unique((__bridge UIWindow*)window)) {} + +Window::~Window() {} + +WindowId Window::GetId() const { + if (!pimpl_->ui_window_) { + return IdAllocator::kInvalidId; + } + + // Store the allocated ID in a static map to ensure consistency + // Note: Use void* as the key to avoid hashing issues for ObjC pointers with libc++ on iOS + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + auto it = window_id_map.find((__bridge void*)pimpl_->ui_window_); + if (it != window_id_map.end()) { + return it->second; + } + + // Allocate new ID using the IdAllocator + WindowId new_id = IdAllocator::Allocate(); + if (new_id != IdAllocator::kInvalidId) { + window_id_map[(__bridge void*)pimpl_->ui_window_] = new_id; + } + return new_id; +} + +void Window::Focus() { + if (pimpl_->ui_window_) { + // On iOS, focus is managed by the view controller and system + [pimpl_->ui_window_ makeKeyWindow]; + } +} + +void Window::Blur() { + if (pimpl_->ui_window_) { + // On iOS, blur is managed by the system + [pimpl_->ui_window_ resignKeyWindow]; + } +} + +bool Window::IsFocused() const { + return pimpl_->ui_window_ && [pimpl_->ui_window_ isKeyWindow]; +} + +void Window::Show() { + if (pimpl_->ui_window_) { + // On iOS, windows are typically shown through view controllers + pimpl_->ui_window_.hidden = NO; + [pimpl_->ui_window_ makeKeyAndVisible]; + } +} + +void Window::ShowInactive() { + if (pimpl_->ui_window_) { + // Same as Show on iOS + Show(); + } +} + +void Window::Hide() { + if (pimpl_->ui_window_) { + pimpl_->ui_window_.hidden = YES; + } +} + +bool Window::IsVisible() const { + return pimpl_->ui_window_ && !pimpl_->ui_window_.hidden; +} + +void Window::Maximize() { + // Maximize is not applicable to iOS (fullscreen is used instead) +} + +void Window::Unmaximize() { + // Unmaximize is not applicable to iOS +} + +bool Window::IsMaximized() const { + return false; +} + +void Window::Minimize() { + // On iOS, minimize sends app to background + if (pimpl_->ui_window_) { + // This would trigger application background mode + } +} + +void Window::Restore() { + // On iOS, restore brings app to foreground + if (pimpl_->ui_window_) { + [pimpl_->ui_window_ makeKeyAndVisible]; + } +} + +bool Window::IsMinimized() const { + // Cannot reliably determine minimized state on iOS + return false; +} + +void Window::SetFullScreen(bool is_full_screen) { + // On iOS, fullscreen is managed through view controller + if (pimpl_->ui_window_) { + UIViewController* rootVC = pimpl_->ui_window_.rootViewController; + if (rootVC) { + rootVC.modalPresentationStyle = + is_full_screen ? UIModalPresentationFullScreen : UIModalPresentationPageSheet; + } + } +} + +bool Window::IsFullScreen() const { + if (!pimpl_->ui_window_) { + return false; + } + + UIViewController* rootVC = pimpl_->ui_window_.rootViewController; + return rootVC && rootVC.modalPresentationStyle == UIModalPresentationFullScreen; +} + +void Window::SetBounds(Rectangle bounds) { + if (pimpl_->ui_window_) { + pimpl_->ui_window_.frame = CGRectMake(bounds.x, bounds.y, bounds.width, bounds.height); + } +} + +Rectangle Window::GetBounds() const { + if (!pimpl_->ui_window_) { + return Rectangle{0.0, 0.0, 0.0, 0.0}; + } + + CGRect frame = pimpl_->ui_window_.frame; + return Rectangle{static_cast(frame.origin.x), static_cast(frame.origin.y), + static_cast(frame.size.width), static_cast(frame.size.height)}; +} + +void Window::SetSize(Size size, bool animate) { + if (pimpl_->ui_window_) { + CGRect frame = pimpl_->ui_window_.frame; + frame.size.width = size.width; + frame.size.height = size.height; + + if (animate) { + [UIView animateWithDuration:0.3 + animations:^{ + pimpl_->ui_window_.frame = frame; + }]; + } else { + pimpl_->ui_window_.frame = frame; + } + } +} + +Size Window::GetSize() const { + if (!pimpl_->ui_window_) { + return Size{0.0, 0.0}; + } + + CGSize size = pimpl_->ui_window_.frame.size; + return Size{static_cast(size.width), static_cast(size.height)}; +} + +void Window::SetContentSize(Size size) { + // On iOS, content size is the same as window size + SetSize(size, false); +} + +Size Window::GetContentSize() const { + return GetSize(); +} + +void Window::SetContentBounds(Rectangle bounds) { + // On iOS, content bounds is the same as window bounds + SetBounds(bounds); +} + +Rectangle Window::GetContentBounds() const { + // On iOS, content bounds is the same as window bounds + return GetBounds(); +} + +void Window::SetMinimumSize(Size size) { + // Not applicable to iOS windows +} + +Size Window::GetMinimumSize() const { + return Size{0, 0}; +} + +void Window::SetMaximumSize(Size size) { + // Not applicable to iOS windows +} + +Size Window::GetMaximumSize() const { + return Size{0, 0}; +} + +void Window::SetResizable(bool is_resizable) { + // iOS windows are not resizable by users +} + +bool Window::IsResizable() const { + return false; +} + +void Window::SetMovable(bool is_movable) { + // iOS windows are not movable by users +} + +bool Window::IsMovable() const { + return false; +} + +void Window::SetMinimizable(bool is_minimizable) { + // iOS manages minimization automatically +} + +bool Window::IsMinimizable() const { + return true; +} + +void Window::SetMaximizable(bool is_maximizable) { + // Maximization is not applicable to iOS +} + +bool Window::IsMaximizable() const { + return false; +} + +void Window::SetFullScreenable(bool is_full_screenable) { + // Fullscreen is controlled by view controller presentation style +} + +bool Window::IsFullScreenable() const { + return true; +} + +void Window::SetClosable(bool is_closable) { + // iOS manages app lifecycle automatically +} + +bool Window::IsClosable() const { + return true; +} + +void Window::SetWindowControlButtonsVisible(bool is_visible) { + // Not applicable to iOS - mobile apps don't have window control buttons +} + +bool Window::IsWindowControlButtonsVisible() const { + // Not applicable to iOS - mobile apps don't have window control buttons + return false; +} + +void Window::SetAlwaysOnTop(bool is_always_on_top) { + // Not applicable to iOS (no multi-window in traditional sense) +} + +bool Window::IsAlwaysOnTop() const { + return false; +} + +void Window::SetPosition(Point point) { + if (pimpl_->ui_window_) { + CGRect frame = pimpl_->ui_window_.frame; + frame.origin.x = point.x; + frame.origin.y = point.y; + pimpl_->ui_window_.frame = frame; + } +} + +Point Window::GetPosition() const { + if (!pimpl_->ui_window_) { + return Point{0, 0}; + } + + CGPoint origin = pimpl_->ui_window_.frame.origin; + return Point{static_cast(origin.x), static_cast(origin.y)}; +} + +void Window::Center() { + if (!pimpl_->ui_window_) + return; + + // Get the screen bounds + UIScreen* screen = pimpl_->ui_window_.screen ?: [UIScreen mainScreen]; + CGRect screenBounds = screen.bounds; + + // Get the current window size + CGRect windowFrame = pimpl_->ui_window_.frame; + + // Calculate center position + CGFloat centerX = (screenBounds.size.width - windowFrame.size.width) / 2.0; + CGFloat centerY = (screenBounds.size.height - windowFrame.size.height) / 2.0; + + // Set the centered frame + windowFrame.origin.x = centerX; + windowFrame.origin.y = centerY; + pimpl_->ui_window_.frame = windowFrame; +} + +void Window::SetTitle(std::string title) { + // iOS windows don't have titles (use view controller title) + if (pimpl_->ui_window_) { + UIViewController* rootVC = pimpl_->ui_window_.rootViewController; + if (rootVC) { + rootVC.title = [NSString stringWithUTF8String:title.c_str()]; + } + } +} + +std::string Window::GetTitle() const { + if (!pimpl_->ui_window_) { + return ""; + } + + UIViewController* rootVC = pimpl_->ui_window_.rootViewController; + if (rootVC && rootVC.title) { + return std::string([rootVC.title UTF8String]); + } + return ""; +} + +void Window::SetTitleBarStyle(TitleBarStyle style) { + // iOS doesn't have traditional title bars + // Use UIViewController.prefersStatusBarHidden or navigation bar appearance instead + NSLog(@"SetTitleBarStyle not applicable on iOS (use status bar or navigation bar APIs)"); +} + +TitleBarStyle Window::GetTitleBarStyle() const { + return TitleBarStyle::Normal; +} + +void Window::SetHasShadow(bool has_shadow) { + // iOS manages shadow automatically +} + +bool Window::HasShadow() const { + return true; +} + +void Window::SetOpacity(float opacity) { + if (pimpl_->ui_window_) { + pimpl_->ui_window_.alpha = opacity; + } +} + +float Window::GetOpacity() const { + return pimpl_->ui_window_ ? pimpl_->ui_window_.alpha : 1.0f; +} + +void Window::SetVisualEffect(VisualEffect effect) { + pimpl_->visual_effect_ = effect; + NSLog(@"SetVisualEffect not supported on iOS"); +} + +VisualEffect Window::GetVisualEffect() const { + return pimpl_->visual_effect_; +} + +void Window::SetBackgroundColor(const Color& color) { + // Not applicable to iOS +} + +Color Window::GetBackgroundColor() const { + return Color::White; +} + +void Window::SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces) { + // Not applicable to iOS +} + +bool Window::IsVisibleOnAllWorkspaces() const { + return false; +} + +void Window::SetIgnoreMouseEvents(bool is_ignore_mouse_events) { + // Not applicable to iOS +} + +bool Window::IsIgnoreMouseEvents() const { + return false; +} + +void Window::SetFocusable(bool is_focusable) { + // iOS manages focus automatically +} + +bool Window::IsFocusable() const { + return true; +} + +void Window::StartDragging() { + // Not applicable to iOS +} + +void Window::StartResizing() { + // Not applicable to iOS +} + +void* Window::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ui_window_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm new file mode 100644 index 0000000..82e11c1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm @@ -0,0 +1,88 @@ +#import +#import +#include "../../window_manager.h" +#include "../../window_registry.h" + +namespace nativeapi { + +class WindowManager::Impl { + public: + Impl(WindowManager* manager) : manager_(manager) {} + WindowManager* manager_; +}; + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + StartEventListening(); +} + +WindowManager::~WindowManager() { + StopEventListening(); +} + +std::shared_ptr WindowManager::Get(WindowId id) { + return WindowRegistry::GetInstance().Get(id); +} + +std::vector> WindowManager::GetAll() { + return WindowRegistry::GetInstance().GetAll(); +} + +std::shared_ptr WindowManager::GetCurrent() { + // Find the first key window + for (const auto& window : WindowRegistry::GetInstance().GetAll()) { + if (window->IsFocused()) { + return window; + } + } + return nullptr; +} + +void WindowManager::SetWillShowHook(std::optional hook) { + // Empty implementation +} + +void WindowManager::SetWillHideHook(std::optional hook) { + // Empty implementation +} + +bool WindowManager::HasWillShowHook() const { + return false; +} + +bool WindowManager::HasWillHideHook() const { + return false; +} + +void WindowManager::HandleWillShow(WindowId id) { + // Empty implementation +} + +void WindowManager::HandleWillHide(WindowId id) { + // Empty implementation +} + +bool WindowManager::CallOriginalShow(WindowId id) { + // iOS doesn't support swizzling for window show/hide + // Return false to indicate unsupported + return false; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + // iOS doesn't support swizzling for window show/hide + // Return false to indicate unsupported + return false; +} + +void WindowManager::StartEventListening() { + // iOS manages window events through UIKit +} + +void WindowManager::StopEventListening() { + // No cleanup needed +} + +void WindowManager::DispatchWindowEvent(const WindowEvent& event) { + Emit(event); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/accessibility_manager_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/accessibility_manager_linux.cpp new file mode 100644 index 0000000..865ebb0 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/accessibility_manager_linux.cpp @@ -0,0 +1,62 @@ +#include "../../accessibility_manager.h" + +#include +#include + +// Import GTK headers for accessibility support +#include +#include + +namespace nativeapi { + +void AccessibilityManager::Enable() { + // On Linux, accessibility is primarily handled by AT-SPI (Assistive + // Technology Service Provider Interface) Applications typically don't need to + // explicitly "enable" accessibility - it's handled by the system However, we + // can ensure GTK accessibility features are available + + // Initialize GTK if not already initialized (for accessibility bridge) + if (!gdk_display_get_default()) { + // Try to initialize GTK silently if not already done + gtk_init_check(nullptr, nullptr); + } + + // The accessibility bridge in GTK is automatically enabled when accessibility + // is needed No explicit action needed - this is a no-op on Linux as + // accessibility is system-managed +} + +bool AccessibilityManager::IsEnabled() { + // Check if accessibility features are enabled on the Linux system + + // Method 1: Check if AT-SPI accessibility bus is running + // This is the most reliable way to detect if accessibility is active + const char* at_spi_bus = g_getenv("AT_SPI_BUS"); + if (at_spi_bus && strlen(at_spi_bus) > 0) { + return true; + } + + // Method 2: Check for common accessibility environment variables + const char* accessibility_enabled = g_getenv("GNOME_ACCESSIBILITY"); + if (accessibility_enabled && g_ascii_strcasecmp(accessibility_enabled, "1") == 0) { + return true; + } + + // Method 3: Check if screen reader (Orca) is running + // This is a fallback method for older systems + if (system("pgrep -x orca > /dev/null 2>&1") == 0) { + return true; + } + + // Method 4: Check GTK accessibility settings if available + // Some desktop environments set this when accessibility is enabled + const char* gtk_modules = g_getenv("GTK_MODULES"); + if (gtk_modules && strstr(gtk_modules, "gail") != nullptr) { + return true; + } + + // If none of the above methods detect accessibility, assume it's disabled + return false; +} + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/application_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/application_linux.cpp new file mode 100644 index 0000000..25f63f6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/application_linux.cpp @@ -0,0 +1,246 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../application.h" +#include "../../menu.h" +#include "../../window_manager.h" + +namespace nativeapi { + +class Application::Impl { + public: + Impl(Application* app) : app_(app), gtk_app_(nullptr), lock_file_handle_(-1) {} + ~Impl() = default; + + bool Initialize() { + // Initialize GTK + gtk_init(nullptr, nullptr); + + // Create GTK application with default ID + gtk_app_ = gtk_application_new("com.nativeapi.application", G_APPLICATION_DEFAULT_FLAGS); + + if (!gtk_app_) { + return false; + } + + // Set default application name + g_object_set(gtk_app_, "application-name", "NativeAPI Application", nullptr); + + // Connect to GTK application signals + g_signal_connect(gtk_app_, "startup", G_CALLBACK(OnStartup), this); + g_signal_connect(gtk_app_, "activate", G_CALLBACK(OnActivate), this); + g_signal_connect(gtk_app_, "shutdown", G_CALLBACK(OnShutdown), this); + + return true; + } + + int Run() { + // Run the GTK main loop + int status = g_application_run(G_APPLICATION(gtk_app_), 0, nullptr); + + return status; + } + + int Run(std::shared_ptr window) { + if (!window) { + return -1; + } + + // Set the window as primary window + app_->SetPrimaryWindow(window); + + // Show the window + window->Show(); + window->Focus(); + + // Run the GTK main loop + int status = g_application_run(G_APPLICATION(gtk_app_), 0, nullptr); + + return status; + } + + void Quit(int exit_code) { g_application_quit(G_APPLICATION(gtk_app_)); } + + bool SetIcon(const std::string& icon_path) { + if (icon_path.empty()) { + return false; + } + + // Load icon from file + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(icon_path.c_str(), nullptr); + if (!pixbuf) { + return false; + } + + // Set application icon + gtk_window_set_default_icon(pixbuf); + + g_object_unref(pixbuf); + return true; + } + + bool SetDockIconVisible(bool visible) { + // Linux doesn't have a dock in the same way as macOS + // This is a no-op for now + return true; + } + + bool SetMenuBar(std::shared_ptr menu) { + if (!menu) { + return false; + } + + // Get the native menu handle + GtkWidget* gtk_menu = static_cast(menu->GetNativeObject()); + if (!gtk_menu) { + return false; + } + + // Note: gtk_application_set_app_menu expects GMenuModel, but our Menu + // class uses legacy GtkMenu widgets. Setting application menu bar is not + // supported with legacy menus in GTK3. Users should add menu bars directly + // to their windows instead. + // TODO: Consider implementing GMenuModel-based menus in the future. + + return false; // Not supported with legacy GtkMenu + } + + void CleanupEventMonitoring() { + // Clean up Linux-specific event monitoring + if (lock_file_handle_ != -1) { + close(lock_file_handle_); + lock_file_handle_ = -1; + } + + if (gtk_app_) { + g_object_unref(gtk_app_); + gtk_app_ = nullptr; + } + } + + private: + Application* app_; + GtkApplication* gtk_app_; + int lock_file_handle_; + + static void OnStartup(GApplication* app, gpointer user_data) { + Impl* impl = static_cast(user_data); + + // Emit application started event + ApplicationStartedEvent event; + impl->app_->Emit(event); + } + + static void OnActivate(GApplication* app, gpointer user_data) { + Impl* impl = static_cast(user_data); + + // Emit application activated event + ApplicationActivatedEvent event; + impl->app_->Emit(event); + } + + static void OnShutdown(GApplication* app, gpointer user_data) { + Impl* impl = static_cast(user_data); + + // Emit application exiting event + ApplicationExitingEvent event(0); + impl->app_->Emit(event); + } +}; + +Application::Application() + : initialized_(true), running_(false), exit_code_(0), pimpl_(std::make_unique(this)) { + // Perform platform-specific initialization automatically + pimpl_->Initialize(); + + // Emit application started event + Emit(); +} + +Application::~Application() { + // Clean up platform-specific event monitoring + pimpl_->CleanupEventMonitoring(); +} + +int Application::Run() { + running_ = true; + + // Start the platform-specific main event loop + int result = pimpl_->Run(); + + running_ = false; + + // Emit exit event + Emit(result); + + return result; +} + +int Application::Run(std::shared_ptr window) { + if (!window) { + return -1; // Invalid window + } + + running_ = true; + + // Start the platform-specific main event loop with window + int result = pimpl_->Run(window); + + running_ = false; + + // Emit exit event + Emit(result); + + return result; +} + +void Application::Quit(int exit_code) { + exit_code_ = exit_code; + + // Emit quit requested event + Emit(); + + // Request platform-specific quit + pimpl_->Quit(exit_code); +} + +bool Application::IsRunning() const { + return running_; +} + +bool Application::IsSingleInstance() const { + return false; +} + +bool Application::SetIcon(const std::string& icon_path) { + return pimpl_->SetIcon(icon_path); +} + +bool Application::SetDockIconVisible(bool visible) { + return pimpl_->SetDockIconVisible(visible); +} + +bool Application::SetMenuBar(std::shared_ptr menu) { + return pimpl_->SetMenuBar(menu); +} + +std::shared_ptr Application::GetPrimaryWindow() const { + return primary_window_; +} + +void Application::SetPrimaryWindow(std::shared_ptr window) { + primary_window_ = window; +} + +std::vector> Application::GetAllWindows() const { + auto& window_manager = WindowManager::GetInstance(); + return window_manager.GetAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/dispatcher_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/dispatcher_linux.cpp new file mode 100644 index 0000000..178fc9d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/dispatcher_linux.cpp @@ -0,0 +1,61 @@ +#include "../../foundation/dispatcher_platform.h" +#include "../../foundation/dispatcher_common.h" + +#include + +namespace nativeapi { +namespace dispatcher_platform { + +namespace { + +gboolean InvokeDispatchedWork(gpointer data) { + auto* work = static_cast*>(data); + if (work) { + (*work)(); + delete work; + } + return G_SOURCE_REMOVE; +} + +} // namespace + +bool PlatformIsMainThread() { + return dispatcher_internal::IsMainThreadByCapturedId(); +} + +void PlatformSetMainThread() { + dispatcher_internal::CaptureCallerAsMainThread(); +} + +bool PlatformIsMainThreadDispatchSupported() { + return true; +} + +bool PlatformRunOnMainThread(std::function fn) { + if (!fn) { + return true; + } + + // g_idle_add() is thread-safe and attaches the source to the default main + // context, which is the context the GTK main loop runs on the main thread. + g_idle_add(InvokeDispatchedWork, new std::function(std::move(fn))); + return true; +} + +bool PlatformRunMainThreadLoopFor(int timeout_ms) { + GMainContext* context = g_main_context_default(); + const gint64 deadline = g_get_monotonic_time() + (gint64)timeout_ms * 1000; + do { + // may_block=FALSE so an empty queue does not stall for the whole budget. + while (g_main_context_iteration(context, FALSE)) { + } + if (timeout_ms <= 0) { + break; + } + g_usleep(1000); + } while (g_get_monotonic_time() < deadline); + return true; +} + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/display_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/display_linux.cpp new file mode 100644 index 0000000..a6b1430 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/display_linux.cpp @@ -0,0 +1,101 @@ +#include "../../display.h" + +#include +#include + +namespace nativeapi { + +// Private implementation class +class Display::Impl { + public: + Impl() = default; + Impl(GdkMonitor* monitor) : gdk_monitor_(monitor) {} + + const DisplayId id_ = IdAllocator::Allocate(); + GdkMonitor* gdk_monitor_ = nullptr; +}; + +Display::Display(void* display) : pimpl_(std::make_unique()) { + if (display) { + pimpl_->gdk_monitor_ = (GdkMonitor*)display; + } +} + +Display::~Display() = default; + +void* Display::GetNativeObjectInternal() const { + return pimpl_->gdk_monitor_; +} + +// Getters - directly read from GdkMonitor +DisplayId Display::GetId() const { + return pimpl_->id_; +} + +std::string Display::GetName() const { + if (!pimpl_->gdk_monitor_) + return ""; + const char* model = gdk_monitor_get_model(pimpl_->gdk_monitor_); + return model ? model : "Unknown"; +} + +Point Display::GetPosition() const { + if (!pimpl_->gdk_monitor_) + return {0.0, 0.0}; + GdkRectangle geometry; + gdk_monitor_get_geometry(pimpl_->gdk_monitor_, &geometry); + return {static_cast(geometry.x), static_cast(geometry.y)}; +} + +Size Display::GetSize() const { + if (!pimpl_->gdk_monitor_) + return {0.0, 0.0}; + GdkRectangle geometry; + gdk_monitor_get_geometry(pimpl_->gdk_monitor_, &geometry); + return {static_cast(geometry.width), static_cast(geometry.height)}; +} + +Rectangle Display::GetWorkArea() const { + if (!pimpl_->gdk_monitor_) + return {0.0, 0.0, 0.0, 0.0}; + GdkRectangle workarea; + gdk_monitor_get_workarea(pimpl_->gdk_monitor_, &workarea); + return {static_cast(workarea.x), static_cast(workarea.y), + static_cast(workarea.width), static_cast(workarea.height)}; +} + +double Display::GetScaleFactor() const { + if (!pimpl_->gdk_monitor_) + return 1.0; + return gdk_monitor_get_scale_factor(pimpl_->gdk_monitor_); +} + +bool Display::IsPrimary() const { + if (!pimpl_->gdk_monitor_) + return false; + GdkDisplay* display = gdk_monitor_get_display(pimpl_->gdk_monitor_); + GdkMonitor* primary = gdk_display_get_primary_monitor(display); + return primary == pimpl_->gdk_monitor_; +} + +DisplayOrientation Display::GetOrientation() const { + if (!pimpl_->gdk_monitor_) + return DisplayOrientation::kPortrait; + GdkRectangle geometry; + gdk_monitor_get_geometry(pimpl_->gdk_monitor_, &geometry); + return (geometry.width > geometry.height) ? DisplayOrientation::kLandscape + : DisplayOrientation::kPortrait; +} + +int Display::GetRefreshRate() const { + if (!pimpl_->gdk_monitor_) + return 60; + int refresh_rate = gdk_monitor_get_refresh_rate(pimpl_->gdk_monitor_); + return refresh_rate > 0 ? refresh_rate / 1000 : 60; // Convert from millihertz to hertz +} + +int Display::GetBitDepth() const { + return 32; // Default for modern displays +} + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/display_manager_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/display_manager_linux.cpp new file mode 100644 index 0000000..d6c8bde --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/display_manager_linux.cpp @@ -0,0 +1,59 @@ +#include +#include +#include + +#include "../../display_manager.h" + +namespace nativeapi { + +DisplayManager::DisplayManager() { + gtk_init(nullptr, nullptr); + // Prime the instance cache so the first change notification diffs against + // the displays present at startup. + GetAll(); + // TODO: Connect to GdkDisplay's "monitor-added" / "monitor-removed" signals + // and call HandleDisplaysChanged() from the handlers. +} + +DisplayManager::~DisplayManager() { + // Destructor implementation +} + +std::vector DisplayManager::EnumerateNativeDisplays() { + std::vector natives; + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + return natives; + } + + GdkMonitor* primary = gdk_display_get_primary_monitor(display); + int monitor_count = gdk_display_get_n_monitors(display); + for (int i = 0; i < monitor_count; ++i) { + GdkMonitor* monitor = gdk_display_get_monitor(display, i); + if (!monitor) { + continue; + } + // A GdkMonitor object is stable for as long as the monitor stays + // connected, so its address serves as the identity key. + bool is_primary = (primary != nullptr) ? (monitor == primary) : (i == 0); + natives.push_back( + {std::to_string(reinterpret_cast(monitor)), monitor, is_primary}); + } + return natives; +} + +Point DisplayManager::GetCursorPosition() { + GdkDisplay* display = gdk_display_get_default(); + GdkSeat* seat = gdk_display_get_default_seat(display); + GdkDevice* pointer = gdk_seat_get_pointer(seat); + + int x, y; + gdk_device_get_position(pointer, NULL, &x, &y); + + Point point; + point.x = x; + point.y = y; + return point; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/image_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/image_linux.cpp new file mode 100644 index 0000000..93391f3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/image_linux.cpp @@ -0,0 +1,272 @@ +#include +#include +#include +#include +#include +#include +#include +#include "../../foundation/geometry.h" +#include "../../image.h" + +namespace nativeapi { + +// Linux-specific implementation of Image class using GdkPixbuf +class Image::Impl { + public: + GdkPixbuf* pixbuf_; + std::string source_; + Size size_; + std::string format_; + + Impl() : pixbuf_(nullptr), size_({0, 0}), format_("Unknown") {} + + ~Impl() { + if (pixbuf_) { + g_object_unref(pixbuf_); + } + } + + Impl(const Impl& other) + : pixbuf_(nullptr), source_(other.source_), size_(other.size_), format_(other.format_) { + if (other.pixbuf_) { + pixbuf_ = gdk_pixbuf_copy(other.pixbuf_); + } + } + + Impl& operator=(const Impl& other) { + if (this != &other) { + if (pixbuf_) { + g_object_unref(pixbuf_); + } + pixbuf_ = nullptr; + source_ = other.source_; + size_ = other.size_; + format_ = other.format_; + if (other.pixbuf_) { + pixbuf_ = gdk_pixbuf_copy(other.pixbuf_); + } + } + return *this; + } +}; + +Image::Image() : pimpl_(std::make_unique()) {} + +Image::~Image() = default; + +Image::Image(const Image& other) : pimpl_(std::make_unique(*other.pimpl_)) {} + +Image::Image(Image&& other) noexcept : pimpl_(std::move(other.pimpl_)) {} + +std::shared_ptr Image::FromFile(const std::string& file_path) { + auto image = std::shared_ptr(new Image()); + + GError* error = nullptr; + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(file_path.c_str(), &error); + + if (pixbuf) { + image->pimpl_->pixbuf_ = pixbuf; + image->pimpl_->source_ = file_path; + + // Get actual image size + int width = gdk_pixbuf_get_width(pixbuf); + int height = gdk_pixbuf_get_height(pixbuf); + image->pimpl_->size_ = {static_cast(width), static_cast(height)}; + + // Determine format from file extension + size_t dotPos = file_path.find_last_of('.'); + if (dotPos != std::string::npos) { + std::string extension = file_path.substr(dotPos + 1); + // Convert to lowercase + for (auto& c : extension) { + c = std::tolower(c); + } + + if (extension == "png") { + image->pimpl_->format_ = "PNG"; + } else if (extension == "jpg" || extension == "jpeg") { + image->pimpl_->format_ = "JPEG"; + } else if (extension == "gif") { + image->pimpl_->format_ = "GIF"; + } else if (extension == "bmp") { + image->pimpl_->format_ = "BMP"; + } else if (extension == "tiff" || extension == "tif") { + image->pimpl_->format_ = "TIFF"; + } else if (extension == "ico") { + image->pimpl_->format_ = "ICO"; + } else if (extension == "svg") { + image->pimpl_->format_ = "SVG"; + } else if (extension == "xpm") { + image->pimpl_->format_ = "XPM"; + } else { + image->pimpl_->format_ = "Unknown"; + } + } + } else { + if (error) { + g_error_free(error); + } + return nullptr; + } + + return image; +} + +// Helper function to decode base64 +static std::vector DecodeBase64(const std::string& base64_data) { + std::vector result; + + gsize out_len = 0; + guchar* decoded = g_base64_decode(base64_data.c_str(), &out_len); + + if (decoded && out_len > 0) { + result.assign(decoded, decoded + out_len); + g_free(decoded); + } + + return result; +} + +std::shared_ptr Image::FromBase64(const std::string& base64_data) { + auto image = std::shared_ptr(new Image()); + + // Remove data URI prefix if present + std::string cleanBase64 = base64_data; + size_t commaPos = base64_data.find(','); + if (commaPos != std::string::npos) { + cleanBase64 = base64_data.substr(commaPos + 1); + } + + // Decode base64 + std::vector imageData = DecodeBase64(cleanBase64); + + if (!imageData.empty()) { + GError* error = nullptr; + GInputStream* stream = + g_memory_input_stream_new_from_data(imageData.data(), imageData.size(), nullptr); + + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_stream(stream, nullptr, &error); + g_object_unref(stream); + + if (pixbuf) { + image->pimpl_->pixbuf_ = pixbuf; + image->pimpl_->source_ = base64_data; + + // Get actual image size + int width = gdk_pixbuf_get_width(pixbuf); + int height = gdk_pixbuf_get_height(pixbuf); + image->pimpl_->size_ = {static_cast(width), static_cast(height)}; + + // Default assumption for base64 images + image->pimpl_->format_ = "PNG"; + } else { + if (error) { + g_error_free(error); + } + return nullptr; + } + } else { + return nullptr; + } + + return image; +} + +Size Image::GetSize() const { + return pimpl_->size_; +} + +std::string Image::GetFormat() const { + return pimpl_->format_; +} + +// Helper function to encode to base64 +static std::string EncodeBase64(const unsigned char* data, size_t length) { + gchar* encoded = g_base64_encode(data, length); + std::string result(encoded); + g_free(encoded); + return result; +} + +std::string Image::ToBase64() const { + if (!pimpl_->pixbuf_) { + return ""; + } + + gchar* buffer = nullptr; + gsize buffer_size = 0; + GError* error = nullptr; + + // Save pixbuf to PNG in memory + gboolean success = + gdk_pixbuf_save_to_buffer(pimpl_->pixbuf_, &buffer, &buffer_size, "png", &error, nullptr); + + if (!success || !buffer) { + if (error) { + g_error_free(error); + } + if (buffer) { + g_free(buffer); + } + return ""; + } + + // Convert to base64 + std::string base64String = EncodeBase64(reinterpret_cast(buffer), buffer_size); + g_free(buffer); + + return "data:image/png;base64," + base64String; +} + +bool Image::SaveToFile(const std::string& file_path) const { + if (!pimpl_->pixbuf_) { + return false; + } + + // Determine file type from extension + size_t dotPos = file_path.find_last_of('.'); + std::string type = "png"; // default + + if (dotPos != std::string::npos) { + std::string extension = file_path.substr(dotPos + 1); + // Convert to lowercase + for (auto& c : extension) { + c = std::tolower(c); + } + + if (extension == "jpg" || extension == "jpeg") { + type = "jpeg"; + } else if (extension == "png") { + type = "png"; + } else if (extension == "bmp") { + type = "bmp"; + } else if (extension == "ico") { + type = "ico"; + } else if (extension == "tiff" || extension == "tif") { + type = "tiff"; + } + } + + GError* error = nullptr; + gboolean success = FALSE; + + if (type == "jpeg") { + // For JPEG, specify quality + success = gdk_pixbuf_save(pimpl_->pixbuf_, file_path.c_str(), type.c_str(), &error, "quality", + "90", nullptr); + } else { + success = gdk_pixbuf_save(pimpl_->pixbuf_, file_path.c_str(), type.c_str(), &error, nullptr); + } + + if (error) { + g_error_free(error); + } + + return success == TRUE; +} + +void* Image::GetNativeObjectInternal() const { + return pimpl_->pixbuf_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/keyboard_monitor_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/keyboard_monitor_linux.cpp new file mode 100644 index 0000000..60bc469 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/keyboard_monitor_linux.cpp @@ -0,0 +1,221 @@ +#include +#include +#include +#include +#include +#include + +#include "../../keyboard_monitor.h" + +// Import X11 headers after including the header to avoid conflicts +// We'll need to undefine None to avoid conflicts with our enum +#include +#include +#include +#include + +// Handle the None conflict from X11 +#ifdef None +#undef None +#endif + +namespace nativeapi { + +class KeyboardMonitor::Impl { + public: + Impl(KeyboardMonitor* monitor) : monitor_(monitor), display_(nullptr), monitoring_(false) {} + + Display* display_; + std::atomic monitoring_; + std::thread monitoring_thread_; + KeyboardMonitor* monitor_; + int xi_opcode_; + + void MonitoringLoop(); + void InitializeXInput(); + void CleanupXInput(); + uint32_t GetModifierState(); +}; + +KeyboardMonitor::KeyboardMonitor() : impl_(std::make_unique(this)) {} + +KeyboardMonitor::~KeyboardMonitor() { + Stop(); +} + +void KeyboardMonitor::Impl::InitializeXInput() { + display_ = XOpenDisplay(nullptr); + if (!display_) { + std::cerr << "Failed to open X display" << std::endl; + return; + } + + // Check for XInput extension + int event, error; + if (!XQueryExtension(display_, "XInputExtension", &xi_opcode_, &event, &error)) { + std::cerr << "XInput extension not available" << std::endl; + XCloseDisplay(display_); + display_ = nullptr; + return; + } + + // Check XInput version + int major = 2, minor = 0; + if (XIQueryVersion(display_, &major, &minor) != Success) { + std::cerr << "XInput 2.0 not available" << std::endl; + XCloseDisplay(display_); + display_ = nullptr; + return; + } + + // Select for keyboard events on root window + XIEventMask eventmask; + unsigned char mask[XIMaskLen(XI_LASTEVENT)] = {0}; + + eventmask.deviceid = XIAllMasterDevices; + eventmask.mask_len = sizeof(mask); + eventmask.mask = mask; + + XISetMask(mask, XI_KeyPress); + XISetMask(mask, XI_KeyRelease); + + Window root = DefaultRootWindow(display_); + if (XISelectEvents(display_, root, &eventmask, 1) != Success) { + std::cerr << "Failed to select XI events" << std::endl; + XCloseDisplay(display_); + display_ = nullptr; + return; + } +} + +void KeyboardMonitor::Impl::CleanupXInput() { + if (display_) { + XCloseDisplay(display_); + display_ = nullptr; + } +} + +uint32_t KeyboardMonitor::Impl::GetModifierState() { + uint32_t modifier_keys = static_cast(ModifierKey::None); + + if (!display_) + return modifier_keys; + + // Query current keyboard state + char keys[32]; + XQueryKeymap(display_, keys); + + // Check for common modifier keycodes + // These keycodes may vary by system, but are common defaults + int shift_keycode = XKeysymToKeycode(display_, XK_Shift_L); + int ctrl_keycode = XKeysymToKeycode(display_, XK_Control_L); + int alt_keycode = XKeysymToKeycode(display_, XK_Alt_L); + int meta_keycode = XKeysymToKeycode(display_, XK_Super_L); + int caps_keycode = XKeysymToKeycode(display_, XK_Caps_Lock); + int num_keycode = XKeysymToKeycode(display_, XK_Num_Lock); + int scroll_keycode = XKeysymToKeycode(display_, XK_Scroll_Lock); + + // Check if keys are pressed + if (shift_keycode && (keys[shift_keycode / 8] & (1 << (shift_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::Shift); + } + if (ctrl_keycode && (keys[ctrl_keycode / 8] & (1 << (ctrl_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::Ctrl); + } + if (alt_keycode && (keys[alt_keycode / 8] & (1 << (alt_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::Alt); + } + if (meta_keycode && (keys[meta_keycode / 8] & (1 << (meta_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::Meta); + } + if (caps_keycode && (keys[caps_keycode / 8] & (1 << (caps_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::CapsLock); + } + if (num_keycode && (keys[num_keycode / 8] & (1 << (num_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::NumLock); + } + if (scroll_keycode && (keys[scroll_keycode / 8] & (1 << (scroll_keycode % 8)))) { + modifier_keys |= static_cast(ModifierKey::ScrollLock); + } + + return modifier_keys; +} + +void KeyboardMonitor::Impl::MonitoringLoop() { + if (!display_) + return; + + while (monitoring_) { + // Check for pending events + while (XPending(display_) && monitoring_) { + XEvent event; + XNextEvent(display_, &event); + + // Handle XI2 events + if (event.xcookie.type == GenericEvent && event.xcookie.extension == xi_opcode_) { + if (XGetEventData(display_, &event.xcookie)) { + XIDeviceEvent* xi_event = (XIDeviceEvent*)event.xcookie.data; + + if (xi_event->evtype == XI_KeyPress) { + KeyPressedEvent key_event(xi_event->detail); + monitor_->Emit(key_event); + + ModifierKeysChangedEvent modifier_event(GetModifierState()); + monitor_->Emit(modifier_event); + } else if (xi_event->evtype == XI_KeyRelease) { + KeyReleasedEvent key_event(xi_event->detail); + monitor_->Emit(key_event); + + ModifierKeysChangedEvent modifier_event(GetModifierState()); + monitor_->Emit(modifier_event); + } + + XFreeEventData(display_, &event.xcookie); + } + } + } + + // Small sleep to prevent excessive CPU usage + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void KeyboardMonitor::Start() { + if (impl_->monitoring_) { + return; // Already started + } + + impl_->InitializeXInput(); + if (!impl_->display_) { + std::cerr << "Failed to initialize X11 display for keyboard monitoring" << std::endl; + return; + } + + impl_->monitoring_ = true; + impl_->monitoring_thread_ = std::thread(&KeyboardMonitor::Impl::MonitoringLoop, impl_.get()); + + std::cout << "Keyboard monitor started successfully" << std::endl; +} + +void KeyboardMonitor::Stop() { + if (!impl_->monitoring_) { + return; // Already stopped + } + + impl_->monitoring_ = false; + + // Wait for monitoring thread to finish + if (impl_->monitoring_thread_.joinable()) { + impl_->monitoring_thread_.join(); + } + + impl_->CleanupXInput(); + + std::cout << "Keyboard monitor stopped successfully" << std::endl; +} + +bool KeyboardMonitor::IsMonitoring() const { + return impl_->monitoring_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/launch_at_login_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/launch_at_login_linux.cpp new file mode 100644 index 0000000..7e7cc16 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/launch_at_login_linux.cpp @@ -0,0 +1,377 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../launch_at_login.h" + +namespace nativeapi { + +namespace { + +// Get HOME directory path +static std::string GetHomeDir() { + const char* home = std::getenv("HOME"); + if (home && *home) { + return std::string(home); + } + struct passwd* pw = getpwuid(getuid()); + if (pw && pw->pw_dir) { + return std::string(pw->pw_dir); + } + return std::string(); +} + +// Return XDG config directory: $XDG_CONFIG_HOME or $HOME/.config +static std::string GetXdgConfigHome() { + const char* xdg = std::getenv("XDG_CONFIG_HOME"); + if (xdg && *xdg) { + return std::string(xdg); + } + std::string home = GetHomeDir(); + if (!home.empty()) { + return home + "/.config"; + } + return std::string(); +} + +// Ensure that a directory exists; create intermediate parents as needed (0755) +static bool EnsureDirExists(const std::string& path) { + if (path.empty()) + return false; + + // Walk through path components creating directories if needed + std::string current; + current.reserve(path.size()); + for (size_t i = 0; i < path.size(); ++i) { + char c = path[i]; + current.push_back(c); + if (c == '/' && !current.empty()) { + if (current.size() > 1) { // skip root "/" + if (mkdir(current.c_str(), 0755) != 0 && errno != EEXIST) { + // Ignore errors if directory already exists + struct stat st{}; + if (stat(current.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) { + return false; + } + } + } + } + } + // Final component (if not ending with '/') + struct stat st{}; + if (stat(path.c_str(), &st) == 0) { + if (S_ISDIR(st.st_mode)) + return true; + } + if (mkdir(path.c_str(), 0755) != 0 && errno != EEXIST) { + // If not EEXIST, it's a failure + if (stat(path.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) { + return false; + } + } + return true; +} + +// Get autostart dir: /autostart +static std::string GetAutostartDir() { + std::string base = GetXdgConfigHome(); + if (base.empty()) + return std::string(); + return base + "/autostart"; +} + +// Sanitize identifier for file name usage: replace '/' and whitespace with '_' +static std::string SanitizeIdForFileName(const std::string& id) { + std::string s = id; + std::replace(s.begin(), s.end(), '/', '_'); + for (char& ch : s) { + if (std::isspace(static_cast(ch))) + ch = '_'; + } + return s; +} + +// Compute desktop file path: /.desktop +static std::string GetDesktopFilePath(const std::string& id) { + std::string dir = GetAutostartDir(); + if (dir.empty()) + return std::string(); + return dir + "/" + SanitizeIdForFileName(id) + ".desktop"; +} + +// Readlink for /proc/self/exe to get absolute program path +static std::string DetectDefaultProgramPath() { + char buf[PATH_MAX] = {0}; + ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (len > 0) { + buf[len] = '\0'; + return std::string(buf); + } + return std::string(); +} + +// Extract program name (basename) from a path +static std::string Basename(const std::string& path) { + if (path.empty()) + return std::string(); + size_t pos = path.find_last_of('/'); + if (pos == std::string::npos) + return path; + if (pos + 1 >= path.size()) + return path; // trailing slash + return path.substr(pos + 1); +} + +// Detect a default identifier: "com.nativeapi.launch_at_login." +static std::string DetectDefaultId() { + std::string prog = DetectDefaultProgramPath(); + std::string name = Basename(prog); + if (name.empty()) + name = "app"; + return "com.nativeapi.launch_at_login." + name; +} + +// Detect default display name: program name +static std::string DetectDefaultDisplayName() { + std::string prog = DetectDefaultProgramPath(); + std::string name = Basename(prog); + if (name.empty()) + name = "Application"; + return name; +} + +// Determine if an argument requires quoting +static bool NeedsQuoting(const std::string& s) { + for (char c : s) { + if (std::isspace(static_cast(c)) || c == '"' || c == '\'' || c == '\\' || + c == '$' || c == '`' || c == '(' || c == ')' || c == '|' || c == '&' || c == ';' || + c == '<' || c == '>' || c == '*' || c == '?' || c == '[' || c == ']' || c == '{' || + c == '}' || c == '~' || c == '!' || c == '#') { + return true; + } + } + return false; +} + +// Quote an argument for Exec= line using double quotes; escape inner " and \ with backslashes. +static std::string QuoteArg(const std::string& s) { + if (!NeedsQuoting(s)) + return s; + std::string out; + out.reserve(s.size() + 2); + out.push_back('"'); + for (char c : s) { + if (c == '"' || c == '\\') { + out.push_back('\\'); + } + out.push_back(c); + } + out.push_back('"'); + return out; +} + +// Join program and arguments into an Exec= line per XDG spec (simple quoting) +static std::string BuildExecLine(const std::string& program, const std::vector& args) { + std::ostringstream oss; + oss << QuoteArg(program); + for (const auto& a : args) { + oss << ' ' << QuoteArg(a); + } + return oss.str(); +} + +// Check if a file exists +static bool FileExists(const std::string& path) { + struct stat st{}; + return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode); +} + +// Write content to file atomically-ish: write to temp, then rename +static bool WriteFileAtomic(const std::string& path, const std::string& content, mode_t mode) { + std::string tmp = path + ".tmp"; + { + std::ofstream ofs(tmp, std::ios::out | std::ios::trunc); + if (!ofs.is_open()) + return false; + ofs << content; + if (!ofs.good()) { + ofs.close(); + unlink(tmp.c_str()); + return false; + } + } + // Set permissions + chmod(tmp.c_str(), mode); + // Rename into place + if (rename(tmp.c_str(), path.c_str()) != 0) { + unlink(tmp.c_str()); + return false; + } + return true; +} + +} // namespace + +class LaunchAtLogin::Impl { + public: + static bool IsSupported() { return true; } + + Impl() + : id_(DetectDefaultId()), + display_name_(DetectDefaultDisplayName()), + program_path_(DetectDefaultProgramPath()) {} + + explicit Impl(const std::string& id) + : id_(id), + display_name_(DetectDefaultDisplayName()), + program_path_(DetectDefaultProgramPath()) {} + + Impl(const std::string& id, const std::string& display_name) + : id_(id), display_name_(display_name), program_path_(DetectDefaultProgramPath()) {} + + ~Impl() = default; + + std::string GetId() const { return id_; } + + std::string GetDisplayName() const { return display_name_; } + + bool SetDisplayName(const std::string& display_name) { + display_name_ = display_name; + return true; + } + + bool SetProgram(const std::string& executable_path, const std::vector& arguments) { + program_path_ = executable_path; + arguments_ = arguments; + return true; + } + + std::string GetExecutablePath() const { return program_path_; } + + std::vector GetArguments() const { return arguments_; } + + bool Enable() { + const std::string dir = GetAutostartDir(); + if (dir.empty()) + return false; + if (!EnsureDirExists(dir)) + return false; + + if (program_path_.empty()) { + program_path_ = DetectDefaultProgramPath(); + if (program_path_.empty()) + return false; + } + + // Build .desktop content + std::ostringstream content; + content << "[Desktop Entry]\n"; + content << "Type=Application\n"; + content << "Name=" << display_name_ << "\n"; + // Optional comment + content << "Comment=LaunchAtLogin entry for " << display_name_ << "\n"; + content << "Exec=" << BuildExecLine(program_path_, arguments_) << "\n"; + content << "X-GNOME-Autostart-enabled=true\n"; + content << "Hidden=false\n"; + // Try to be friendly with common desktops + content << "X-KDE-autostart-after=panel\n"; + + const std::string path = GetDesktopFilePath(id_); + if (path.empty()) + return false; + + // Write file with 0644 + if (!WriteFileAtomic(path, content.str(), 0644)) { + return false; + } + + return true; + } + + bool Disable() { + const std::string path = GetDesktopFilePath(id_); + if (path.empty()) + return false; + if (FileExists(path)) { + if (unlink(path.c_str()) != 0) { + return false; + } + } + return true; + } + + bool IsEnabled() const { + const std::string path = GetDesktopFilePath(id_); + return FileExists(path); + } + + private: + std::string id_; + std::string display_name_; + std::string program_path_; + std::vector arguments_; +}; + +// LaunchAtLogin public API implementations + +LaunchAtLogin::LaunchAtLogin() : pimpl_(std::make_unique()) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id) : pimpl_(std::make_unique(id)) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id, const std::string& display_name) + : pimpl_(std::make_unique(id, display_name)) {} + +LaunchAtLogin::~LaunchAtLogin() = default; + +bool LaunchAtLogin::IsSupported() { + return Impl::IsSupported(); +} + +std::string LaunchAtLogin::GetId() const { + return pimpl_->GetId(); +} + +std::string LaunchAtLogin::GetDisplayName() const { + return pimpl_->GetDisplayName(); +} + +bool LaunchAtLogin::SetDisplayName(const std::string& display_name) { + return pimpl_->SetDisplayName(display_name); +} + +bool LaunchAtLogin::SetProgram(const std::string& executable_path, + const std::vector& arguments) { + return pimpl_->SetProgram(executable_path, arguments); +} + +std::string LaunchAtLogin::GetExecutablePath() const { + return pimpl_->GetExecutablePath(); +} + +std::vector LaunchAtLogin::GetArguments() const { + return pimpl_->GetArguments(); +} + +bool LaunchAtLogin::Enable() { + return pimpl_->Enable(); +} + +bool LaunchAtLogin::Disable() { + return pimpl_->Disable(); +} + +bool LaunchAtLogin::IsEnabled() const { + return pimpl_->IsEnabled(); +} + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/menu_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/menu_linux.cpp new file mode 100644 index 0000000..4896337 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/menu_linux.cpp @@ -0,0 +1,893 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../foundation/id_allocator.h" +#include "../../image.h" +#include "../../menu.h" +#include "../../window.h" + +namespace nativeapi { + +// GTK signal handlers → Event emission +static void OnGtkMenuItemActivate(GtkMenuItem* /*item*/, gpointer user_data) { + MenuItem* menu_item = static_cast(user_data); + if (!menu_item) { + return; + } + menu_item->Emit(MenuItemClickedEvent(menu_item->GetId())); +} + +// For checkbox and radio items we listen to "toggled" to avoid recursive +// activate emissions when GTK internally updates group state. For radio items, +// we only emit when the item becomes active. +static void OnGtkCheckMenuItemToggled(GtkCheckMenuItem* item, gpointer user_data) { + MenuItem* menu_item = static_cast(user_data); + if (!menu_item) { + return; + } + gboolean active = gtk_check_menu_item_get_active(item); + if (menu_item->GetType() == MenuItemType::Radio) { + if (active) { + menu_item->Emit(MenuItemClickedEvent(menu_item->GetId())); + } + } else { + // Checkbox: emit on any toggle + menu_item->Emit(MenuItemClickedEvent(menu_item->GetId())); + } +} + +static void OnGtkMenuMap(GtkWidget* /*menu*/, gpointer user_data) { + Menu* menu_obj = static_cast(user_data); + if (!menu_obj) { + return; + } + menu_obj->Emit(MenuOpenedEvent(menu_obj->GetId())); +} + +static void OnGtkMenuUnmap(GtkWidget* /*menu*/, gpointer user_data) { + Menu* menu_obj = static_cast(user_data); + if (!menu_obj) { + return; + } + menu_obj->Emit(MenuClosedEvent(menu_obj->GetId())); +} + +static void OnGtkSubmenuMap(GtkWidget* /*submenu*/, gpointer user_data) { + MenuItem* menu_item = static_cast(user_data); + if (!menu_item) { + return; + } + // Emit submenu opened on the item + menu_item->Emit(MenuItemSubmenuOpenedEvent(menu_item->GetId())); +} + +static void OnGtkSubmenuUnmap(GtkWidget* /*submenu*/, gpointer user_data) { + MenuItem* menu_item = static_cast(user_data); + if (!menu_item) { + return; + } + // Emit submenu closed on the item + menu_item->Emit(MenuItemSubmenuClosedEvent(menu_item->GetId())); +} + +// Private implementation class for MenuItem +class MenuItem::Impl { + public: + Impl(MenuItemId id, GtkWidget* menu_item, MenuItemType type) + : id_(id), + gtk_menu_item_(menu_item), + title_(""), + tooltip_(""), + type_(type), + state_(MenuItemState::Unchecked), + radio_group_(-1), + accelerator_("", ModifierKey::None), + activate_handler_id_(0), + toggled_handler_id_(0) {} + + void ApplyRadioGroup() { + if (!gtk_menu_item_ || type_ != MenuItemType::Radio || radio_group_ < 0) { + return; + } + + std::lock_guard lock(s_group_map_mutex_); + + GSList* target_group = nullptr; + auto it = s_group_map_.find(radio_group_); + if (it != s_group_map_.end()) { + target_group = it->second; + } else { + target_group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(gtk_menu_item_)); + s_group_map_[radio_group_] = target_group; + } + + gtk_radio_menu_item_set_group(GTK_RADIO_MENU_ITEM(gtk_menu_item_), target_group); + + // Update stored head pointer after potential re-linking + s_group_map_[radio_group_] = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(gtk_menu_item_)); + } + + MenuItemId id_; + GtkWidget* gtk_menu_item_; + std::optional title_; + std::shared_ptr image_; + std::optional tooltip_; + MenuItemType type_; + MenuItemState state_; + int radio_group_; + KeyboardAccelerator accelerator_; + std::shared_ptr submenu_; + + // Signal handler IDs for cleanup + gulong activate_handler_id_; + gulong toggled_handler_id_; + + // Shared map from logical group id to GTK group list + static std::unordered_map s_group_map_; + static std::mutex s_group_map_mutex_; +}; + +// Static member definitions +std::unordered_map MenuItem::Impl::s_group_map_; +std::mutex MenuItem::Impl::s_group_map_mutex_; + +MenuItem::MenuItem(const std::string& label, MenuItemType type) { + MenuItemId id = IdAllocator::Allocate(); + GtkWidget* gtk_item = nullptr; + + switch (type) { + case MenuItemType::Separator: + gtk_item = gtk_separator_menu_item_new(); + break; + case MenuItemType::Checkbox: + gtk_item = gtk_check_menu_item_new_with_label(label.c_str()); + break; + case MenuItemType::Radio: + gtk_item = gtk_radio_menu_item_new_with_label(nullptr, label.c_str()); + break; + case MenuItemType::Normal: + case MenuItemType::Submenu: + default: + gtk_item = gtk_menu_item_new_with_label(label.c_str()); + break; + } + + pimpl_ = std::unique_ptr(new Impl(id, gtk_item, type)); + + if (!label.empty()) { + pimpl_->title_ = label; + } else { + pimpl_->title_.reset(); + } + + // Connect signals for click/toggle events (except separators) + if (gtk_item && type != MenuItemType::Separator) { + if (type == MenuItemType::Checkbox || type == MenuItemType::Radio) { + pimpl_->toggled_handler_id_ = g_signal_connect(G_OBJECT(gtk_item), "toggled", + G_CALLBACK(OnGtkCheckMenuItemToggled), this); + } else { + pimpl_->activate_handler_id_ = + g_signal_connect(G_OBJECT(gtk_item), "activate", G_CALLBACK(OnGtkMenuItemActivate), this); + } + } +} + +MenuItem::MenuItem(void* menu_item) { + MenuItemId id = IdAllocator::Allocate(); + pimpl_ = std::unique_ptr(new Impl(id, (GtkWidget*)menu_item, MenuItemType::Normal)); + if (pimpl_->gtk_menu_item_ && pimpl_->type_ != MenuItemType::Separator) { + const char* label = gtk_menu_item_get_label(GTK_MENU_ITEM(pimpl_->gtk_menu_item_)); + if (label && label[0] != '\0') { + pimpl_->title_ = std::string(label); + } else { + pimpl_->title_.reset(); + } + } + + if (pimpl_->gtk_menu_item_) { + if (GTK_IS_CHECK_MENU_ITEM(pimpl_->gtk_menu_item_)) { + pimpl_->toggled_handler_id_ = g_signal_connect(G_OBJECT(pimpl_->gtk_menu_item_), "toggled", + G_CALLBACK(OnGtkCheckMenuItemToggled), this); + } else { + pimpl_->activate_handler_id_ = g_signal_connect(G_OBJECT(pimpl_->gtk_menu_item_), "activate", + G_CALLBACK(OnGtkMenuItemActivate), this); + } + } +} + +MenuItem::~MenuItem() { + // Disconnect signal handlers before destruction to prevent accessing freed memory + if (pimpl_->gtk_menu_item_) { + // Disconnect submenu map/unmap handlers first if they exist + if (pimpl_->submenu_ && pimpl_->submenu_->GetNativeObject()) { + GtkWidget* submenu_widget = (GtkWidget*)pimpl_->submenu_->GetNativeObject(); + if (submenu_widget && GTK_IS_WIDGET(submenu_widget)) { + g_signal_handlers_disconnect_by_func(G_OBJECT(submenu_widget), (gpointer)OnGtkSubmenuMap, + this); + g_signal_handlers_disconnect_by_func(G_OBJECT(submenu_widget), (gpointer)OnGtkSubmenuUnmap, + this); + } + } + + // Disconnect item-specific signal handlers + if (pimpl_->activate_handler_id_ > 0) { + g_signal_handler_disconnect(G_OBJECT(pimpl_->gtk_menu_item_), pimpl_->activate_handler_id_); + pimpl_->activate_handler_id_ = 0; + } + if (pimpl_->toggled_handler_id_ > 0) { + g_signal_handler_disconnect(G_OBJECT(pimpl_->gtk_menu_item_), pimpl_->toggled_handler_id_); + pimpl_->toggled_handler_id_ = 0; + } + + // Note: We don't destroy the gtk_menu_item_ here because it's owned by the parent Menu + // and will be destroyed when the Menu container is destroyed + } +} + +MenuItemId MenuItem::GetId() const { + return pimpl_->id_; +} + +MenuItemType MenuItem::GetType() const { + return pimpl_->type_; +} + +void MenuItem::SetLabel(const std::optional& label) { + pimpl_->title_ = label; + if (pimpl_->gtk_menu_item_ && pimpl_->type_ != MenuItemType::Separator) { + const char* labelStr = label.has_value() ? label->c_str() : ""; + + // Check if we have a custom box layout (with icon) + GtkWidget* child = gtk_bin_get_child(GTK_BIN(pimpl_->gtk_menu_item_)); + if (child && GTK_IS_BOX(child)) { + // Custom layout with icon - find the label widget and update it + GList* children = gtk_container_get_children(GTK_CONTAINER(child)); + for (GList* iter = children; iter != nullptr; iter = iter->next) { + GtkWidget* widget = GTK_WIDGET(iter->data); + if (GTK_IS_LABEL(widget)) { + gtk_label_set_text(GTK_LABEL(widget), labelStr); + break; + } + } + g_list_free(children); + } else { + // Simple label-only layout + gtk_menu_item_set_label(GTK_MENU_ITEM(pimpl_->gtk_menu_item_), labelStr); + } + } +} + +std::optional MenuItem::GetLabel() const { + return pimpl_->title_; +} + +void MenuItem::SetIcon(std::shared_ptr image) { + pimpl_->image_ = image; + + if (!pimpl_->gtk_menu_item_ || pimpl_->type_ == MenuItemType::Separator) { + return; + } + + // Get current label text to preserve it - prefer stored title over GTK widget + std::string current_label; + if (pimpl_->title_.has_value()) { + current_label = pimpl_->title_.value(); + } else { + // Fallback to GTK widget if title not set + const char* label_text = gtk_menu_item_get_label(GTK_MENU_ITEM(pimpl_->gtk_menu_item_)); + current_label = label_text ? label_text : ""; + } + + // Remove existing child widget + GtkWidget* existing_child = gtk_bin_get_child(GTK_BIN(pimpl_->gtk_menu_item_)); + if (existing_child) { + gtk_container_remove(GTK_CONTAINER(pimpl_->gtk_menu_item_), existing_child); + } + + if (image && image->GetNativeObject()) { + // Create a horizontal box to hold icon and label + GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); // 6px spacing + + // Get the GdkPixbuf from the image + GdkPixbuf* pixbuf = static_cast(image->GetNativeObject()); + + // Scale the icon to a reasonable menu size (16x16 is standard for menu items) + const int icon_size = 16; + GdkPixbuf* scaled_pixbuf = nullptr; + + int original_width = gdk_pixbuf_get_width(pixbuf); + int original_height = gdk_pixbuf_get_height(pixbuf); + + if (original_width != icon_size || original_height != icon_size) { + scaled_pixbuf = gdk_pixbuf_scale_simple(pixbuf, icon_size, icon_size, GDK_INTERP_BILINEAR); + } else { + scaled_pixbuf = gdk_pixbuf_copy(pixbuf); + } + + // Create GtkImage from the pixbuf + GtkWidget* gtk_image = gtk_image_new_from_pixbuf(scaled_pixbuf); + g_object_unref(scaled_pixbuf); // GtkImage takes its own reference + + // Create label widget + GtkWidget* label = gtk_label_new(current_label.c_str()); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); // Left-align the label + + // Pack icon and label into box + gtk_box_pack_start(GTK_BOX(box), gtk_image, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0); + + // Add box to menu item + gtk_container_add(GTK_CONTAINER(pimpl_->gtk_menu_item_), box); + gtk_widget_show_all(box); + } else { + // No icon - restore simple label display + GtkWidget* label = gtk_label_new(current_label.c_str()); + gtk_label_set_xalign(GTK_LABEL(label), 0.0); + gtk_container_add(GTK_CONTAINER(pimpl_->gtk_menu_item_), label); + gtk_widget_show(label); + } +} + +std::shared_ptr MenuItem::GetIcon() const { + return pimpl_->image_; +} + +void MenuItem::SetTooltip(const std::optional& tooltip) { + pimpl_->tooltip_ = tooltip; + if (pimpl_->gtk_menu_item_) { + if (tooltip.has_value()) { + gtk_widget_set_tooltip_text(pimpl_->gtk_menu_item_, tooltip->c_str()); + } else { + gtk_widget_set_tooltip_text(pimpl_->gtk_menu_item_, nullptr); + } + } +} + +std::optional MenuItem::GetTooltip() const { + return pimpl_->tooltip_; +} + +void MenuItem::SetAccelerator(const std::optional& accelerator) { + if (accelerator.has_value()) { + pimpl_->accelerator_ = *accelerator; + } else { + pimpl_->accelerator_ = KeyboardAccelerator("", ModifierKey::None); + } + // TODO: Implement GTK accelerator setting +} + +KeyboardAccelerator MenuItem::GetAccelerator() const { + return pimpl_->accelerator_; +} + +void MenuItem::SetEnabled(bool enabled) { + if (pimpl_->gtk_menu_item_) { + gtk_widget_set_sensitive(pimpl_->gtk_menu_item_, enabled ? TRUE : FALSE); + } +} + +bool MenuItem::IsEnabled() const { + if (pimpl_->gtk_menu_item_) { + return gtk_widget_get_sensitive(pimpl_->gtk_menu_item_) == TRUE; + } + return true; +} + +void MenuItem::SetState(MenuItemState state) { + pimpl_->state_ = state; + if (pimpl_->gtk_menu_item_) { + if (pimpl_->type_ == MenuItemType::Checkbox) { + // Update checked state + gboolean active = (state == MenuItemState::Checked) ? TRUE : FALSE; + // Block the "toggled" signal to prevent recursive triggering when setting active + g_signal_handlers_block_by_func(G_OBJECT(pimpl_->gtk_menu_item_), + (gpointer)OnGtkCheckMenuItemToggled, this); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(pimpl_->gtk_menu_item_), active); + g_signal_handlers_unblock_by_func(G_OBJECT(pimpl_->gtk_menu_item_), + (gpointer)OnGtkCheckMenuItemToggled, this); + + // Reflect tri-state (Mixed) visually using GTK's inconsistent state + gtk_check_menu_item_set_inconsistent(GTK_CHECK_MENU_ITEM(pimpl_->gtk_menu_item_), + (state == MenuItemState::Mixed) ? TRUE : FALSE); + } else if (pimpl_->type_ == MenuItemType::Radio) { + gboolean active = (state == MenuItemState::Checked) ? TRUE : FALSE; + // Block the "toggled" signal to prevent recursive triggering + g_signal_handlers_block_by_func(G_OBJECT(pimpl_->gtk_menu_item_), + (gpointer)OnGtkCheckMenuItemToggled, this); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(pimpl_->gtk_menu_item_), active); + g_signal_handlers_unblock_by_func(G_OBJECT(pimpl_->gtk_menu_item_), + (gpointer)OnGtkCheckMenuItemToggled, this); + } + } +} + +MenuItemState MenuItem::GetState() const { + // For checkbox and radio items, get the actual state from GTK widget + if (pimpl_->gtk_menu_item_ && + (pimpl_->type_ == MenuItemType::Checkbox || pimpl_->type_ == MenuItemType::Radio)) { + if (pimpl_->type_ == MenuItemType::Checkbox) { + // If inconsistent is set, treat as Mixed regardless of active + gboolean inconsistent = + gtk_check_menu_item_get_inconsistent(GTK_CHECK_MENU_ITEM(pimpl_->gtk_menu_item_)); + if (inconsistent) { + return MenuItemState::Mixed; + } + } + + gboolean active = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(pimpl_->gtk_menu_item_)); + return active ? MenuItemState::Checked : MenuItemState::Unchecked; + } + return pimpl_->state_; +} + +void MenuItem::SetRadioGroup(int group_id) { + pimpl_->radio_group_ = group_id; + pimpl_->ApplyRadioGroup(); +} + +int MenuItem::GetRadioGroup() const { + return pimpl_->radio_group_; +} + +void MenuItem::SetSubmenu(std::shared_ptr submenu) { + pimpl_->submenu_ = submenu; + if (pimpl_->gtk_menu_item_ && submenu) { + gtk_menu_item_set_submenu(GTK_MENU_ITEM(pimpl_->gtk_menu_item_), + (GtkWidget*)submenu->GetNativeObject()); + + // Emit submenu open/close events on the parent item when submenu + // maps/unmaps (actual visibility on screen) + GtkWidget* submenu_widget = (GtkWidget*)submenu->GetNativeObject(); + if (submenu_widget) { + g_signal_connect(G_OBJECT(submenu_widget), "map", G_CALLBACK(OnGtkSubmenuMap), this); + g_signal_connect(G_OBJECT(submenu_widget), "unmap", G_CALLBACK(OnGtkSubmenuUnmap), this); + } + } +} + +std::shared_ptr MenuItem::GetSubmenu() const { + return pimpl_->submenu_; +} + +void* MenuItem::GetNativeObjectInternal() const { + return (void*)pimpl_->gtk_menu_item_; +} + +// Private implementation class for Menu +class Menu::Impl { + public: + Impl(MenuId id, GtkWidget* menu) + : id_(id), gtk_menu_(menu), map_handler_id_(0), unmap_handler_id_(0) {} + + MenuId id_; + GtkWidget* gtk_menu_; + std::vector> items_; + + // Signal handler IDs for cleanup + gulong map_handler_id_; + gulong unmap_handler_id_; +}; + +Menu::Menu() { + MenuId id = IdAllocator::Allocate(); + pimpl_ = std::unique_ptr(new Impl(id, gtk_menu_new())); + // Connect menu map/unmap to emit open/close events when actually visible + if (pimpl_->gtk_menu_) { + pimpl_->map_handler_id_ = + g_signal_connect(G_OBJECT(pimpl_->gtk_menu_), "map", G_CALLBACK(OnGtkMenuMap), this); + pimpl_->unmap_handler_id_ = + g_signal_connect(G_OBJECT(pimpl_->gtk_menu_), "unmap", G_CALLBACK(OnGtkMenuUnmap), this); + } +} + +Menu::Menu(void* menu) { + MenuId id = IdAllocator::Allocate(); + pimpl_ = std::unique_ptr(new Impl(id, (GtkWidget*)menu)); + if (pimpl_->gtk_menu_) { + pimpl_->map_handler_id_ = + g_signal_connect(G_OBJECT(pimpl_->gtk_menu_), "map", G_CALLBACK(OnGtkMenuMap), this); + pimpl_->unmap_handler_id_ = + g_signal_connect(G_OBJECT(pimpl_->gtk_menu_), "unmap", G_CALLBACK(OnGtkMenuUnmap), this); + } +} + +Menu::~Menu() { + // Disconnect signal handlers and properly clean up GTK widget + if (pimpl_->gtk_menu_) { + // Ensure menu is closed before destroying to prevent processing events on freed widget + if (gtk_widget_get_visible(pimpl_->gtk_menu_)) { + gtk_menu_popdown(GTK_MENU(pimpl_->gtk_menu_)); + } + + // Disconnect signal handlers before destroying to prevent accessing freed memory + if (pimpl_->map_handler_id_ > 0) { + g_signal_handler_disconnect(G_OBJECT(pimpl_->gtk_menu_), pimpl_->map_handler_id_); + pimpl_->map_handler_id_ = 0; + } + if (pimpl_->unmap_handler_id_ > 0) { + g_signal_handler_disconnect(G_OBJECT(pimpl_->gtk_menu_), pimpl_->unmap_handler_id_); + pimpl_->unmap_handler_id_ = 0; + } + + // Use gtk_widget_destroy() instead of g_object_unref() to properly clean up + // the widget hierarchy and ensure all pending events are handled before destruction + gtk_widget_destroy(pimpl_->gtk_menu_); + pimpl_->gtk_menu_ = nullptr; + } +} + +MenuId Menu::GetId() const { + return pimpl_->id_; +} + +void Menu::AddItem(std::shared_ptr item) { + if (pimpl_->gtk_menu_ && item && item->GetNativeObject()) { + pimpl_->items_.push_back(item); + gtk_menu_shell_append(GTK_MENU_SHELL(pimpl_->gtk_menu_), (GtkWidget*)item->GetNativeObject()); + } +} + +void Menu::InsertItem(size_t index, std::shared_ptr item) { + if (!item) + return; + + if (index >= pimpl_->items_.size()) { + AddItem(item); + return; + } + + pimpl_->items_.insert(pimpl_->items_.begin() + index, item); + if (pimpl_->gtk_menu_ && item->GetNativeObject()) { + gtk_menu_shell_insert(GTK_MENU_SHELL(pimpl_->gtk_menu_), (GtkWidget*)item->GetNativeObject(), + index); + } +} + +bool Menu::RemoveItem(std::shared_ptr item) { + if (pimpl_->gtk_menu_ && item && item->GetNativeObject()) { + auto it = std::find(pimpl_->items_.begin(), pimpl_->items_.end(), item); + if (it != pimpl_->items_.end()) { + pimpl_->items_.erase(it); + gtk_container_remove(GTK_CONTAINER(pimpl_->gtk_menu_), (GtkWidget*)item->GetNativeObject()); + return true; + } + } + return false; +} + +bool Menu::RemoveItemById(MenuItemId item_id) { + for (auto& item : pimpl_->items_) { + if (item->GetId() == item_id) { + return RemoveItem(item); + } + } + return false; +} + +bool Menu::RemoveItemAt(size_t index) { + if (index < pimpl_->items_.size()) { + auto item = pimpl_->items_[index]; + return RemoveItem(item); + } + return false; +} + +void Menu::Clear() { + while (!pimpl_->items_.empty()) { + RemoveItem(pimpl_->items_.back()); + } +} + +void Menu::AddSeparator() { + auto separator = std::make_shared("", MenuItemType::Separator); + AddItem(separator); +} + +void Menu::InsertSeparator(size_t index) { + auto separator = std::make_shared("", MenuItemType::Separator); + InsertItem(index, separator); +} + +size_t Menu::GetItemCount() const { + return pimpl_->items_.size(); +} + +std::shared_ptr Menu::GetItemAt(size_t index) const { + if (index < pimpl_->items_.size()) { + return pimpl_->items_[index]; + } + return nullptr; +} + +std::shared_ptr Menu::GetItemById(MenuItemId item_id) const { + for (const auto& item : pimpl_->items_) { + if (item->GetId() == item_id) { + return item; + } + } + return nullptr; +} + +std::vector> Menu::GetAllItems() const { + return pimpl_->items_; +} + +bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { + // Ensure GTK operations run on the main thread (owner of default GMainContext) + if (!g_main_context_is_owner(g_main_context_default())) { + struct OpenInvokeData { + Menu* self; + PositioningStrategy strategy; + Placement placement; + bool result; + GMutex mutex; + GCond cond; + bool done; + } data{this, strategy, placement, false}; + + g_mutex_init(&data.mutex); + g_cond_init(&data.cond); + data.done = false; + + g_mutex_lock(&data.mutex); + g_main_context_invoke( + nullptr, + [](gpointer user_data) -> gboolean { + OpenInvokeData* d = static_cast(user_data); + bool r = d->self->Open(d->strategy, d->placement); + g_mutex_lock(&d->mutex); + d->result = r; + d->done = true; + g_cond_signal(&d->cond); + g_mutex_unlock(&d->mutex); + return G_SOURCE_REMOVE; + }, + &data); + + while (!data.done) { + g_cond_wait(&data.cond, &data.mutex); + } + g_mutex_unlock(&data.mutex); + g_cond_clear(&data.cond); + g_mutex_clear(&data.mutex); + return data.result; + } + + if (!pimpl_->gtk_menu_) { + return false; + } + + gtk_widget_show_all(pimpl_->gtk_menu_); + + // Get GdkWindow from relative window if available, otherwise use root window + GdkWindow* gdk_window = nullptr; + const Window* relative_window = strategy.GetRelativeWindow(); + if (relative_window) { + void* native_obj = relative_window->GetNativeObject(); + if (native_obj) { + gdk_window = static_cast(native_obj); + } + } + if (!gdk_window) { + gdk_window = gdk_get_default_root_window(); + } + if (!gdk_window) { + // No window available (e.g., Wayland without root window) → cannot show + return false; + } + + GdkRectangle rectangle; + + switch (strategy.GetType()) { + case PositioningStrategy::Type::Absolute: + // Linux does not support Absolute positioning strategy + std::cerr << "Warning: Absolute positioning strategy is not supported on Linux" << std::endl; + return false; + + case PositioningStrategy::Type::CursorPosition: { + // Position relative to the window under the pointer to avoid coord space mismatches + int x = 0, y = 0; + GdkWindow* pointer_window = nullptr; +#if GTK_CHECK_VERSION(3, 20, 0) + GdkDisplay* display = gdk_display_get_default(); + GdkSeat* seat = display ? gdk_display_get_default_seat(display) : nullptr; + GdkDevice* pointer = seat ? gdk_seat_get_pointer(seat) : nullptr; + if (pointer) { + pointer_window = gdk_device_get_window_at_position(pointer, &x, &y); + } +#else + GdkDeviceManager* devman = gdk_display_get_device_manager(gdk_display_get_default()); + GdkDevice* pointer = gdk_device_manager_get_client_pointer(devman); + if (pointer) { + GdkScreen* screen = nullptr; + gdk_device_get_position(pointer, &screen, &x, &y); // screen coords + pointer_window = gdk_get_default_root_window(); + } +#endif + if (pointer_window) { + gdk_window = pointer_window; // ensure rect coords match this window + } else if (!gdk_window) { + gdk_window = gdk_get_default_root_window(); + } + + rectangle.x = x; + rectangle.y = y; + rectangle.width = 1; + rectangle.height = 1; + break; + } + + case PositioningStrategy::Type::Relative: { + // Relative positioning + Rectangle rect = strategy.GetRelativeRectangle(); + Point offset = strategy.GetRelativeOffset(); + Point position = Point{rect.x + offset.x, rect.y + offset.y}; + + // If we have a relative window, adjust for frame extents and title bar + if (relative_window && relative_window->GetNativeObject()) { + GdkRectangle frame_rectangle; + gdk_window_get_frame_extents(gdk_window, &frame_rectangle); + + // Get GtkWindow for window position and title bar + GtkWindow* gtk_window = nullptr; + GList* toplevels = gtk_window_list_toplevels(); + for (GList* l = toplevels; l != nullptr; l = l->next) { + GtkWindow* candidate = GTK_WINDOW(l->data); + GdkWindow* candidate_gdk = gtk_widget_get_window(GTK_WIDGET(candidate)); + if (candidate_gdk == gdk_window) { + gtk_window = candidate; + break; + } + } + g_list_free(toplevels); + + // Get window position using gtk_window_get_position (works better on Wayland) + gint window_x = 0, window_y = 0; + if (gtk_window) { + gtk_window_get_position(gtk_window, &window_x, &window_y); + } else { + // Fallback to gdk_window_get_origin if gtk_window not found + gdk_window_get_origin(gdk_window, &window_x, &window_y); + } + + // Get title bar height from GtkWindow + int title_bar_height = 0; + if (gtk_window) { + GtkWidget* titlebar = gtk_window_get_titlebar(gtk_window); + if (titlebar) { + title_bar_height = gtk_widget_get_allocated_height(titlebar); + } + } + + // Get device pixel ratio for DPI scaling + double device_pixel_ratio = 1.0; + GdkScreen* screen = gdk_window_get_screen(gdk_window); + if (screen) { + // Get scale factor (typically 1.0 for standard DPI, 2.0 for HiDPI) + device_pixel_ratio = gdk_screen_get_resolution(screen) / 96.0; + if (device_pixel_ratio <= 0.0) { + device_pixel_ratio = 1.0; + } + } + + // Convert content-relative coordinates to window-relative coordinates + // Apply DPI scaling, then adjust for window position and frame extents + rectangle.x = + static_cast((position.x * device_pixel_ratio) + window_x - frame_rectangle.x); + rectangle.y = static_cast((position.y * device_pixel_ratio) + window_y - + frame_rectangle.y + title_bar_height); + } else { + // Relative to rectangle (no window) - use root window coordinates + rectangle.x = static_cast(position.x); + rectangle.y = static_cast(position.y); + } + + // Set rectangle dimensions + rectangle.width = 1; + rectangle.height = 1; + break; + } + + default: + return false; + } + + // Map placement to GDK gravity (menu anchor) + GdkGravity menu_anchor = GDK_GRAVITY_NORTH_WEST; + + switch (placement) { + case Placement::TopStart: + case Placement::Top: + menu_anchor = GDK_GRAVITY_SOUTH_WEST; + break; + case Placement::TopEnd: + menu_anchor = GDK_GRAVITY_SOUTH_EAST; + break; + case Placement::BottomStart: + case Placement::Bottom: + menu_anchor = GDK_GRAVITY_NORTH_WEST; + break; + case Placement::BottomEnd: + menu_anchor = GDK_GRAVITY_NORTH_EAST; + break; + case Placement::LeftStart: + case Placement::Left: + menu_anchor = GDK_GRAVITY_NORTH_EAST; + break; + case Placement::LeftEnd: + menu_anchor = GDK_GRAVITY_SOUTH_EAST; + break; + case Placement::RightStart: + case Placement::Right: + menu_anchor = GDK_GRAVITY_NORTH_WEST; + break; + case Placement::RightEnd: + menu_anchor = GDK_GRAVITY_SOUTH_WEST; + break; + } + + // Position menu using gtk_menu_popup_at_rect + gtk_menu_popup_at_rect(GTK_MENU(pimpl_->gtk_menu_), gdk_window, &rectangle, + GDK_GRAVITY_NORTH_WEST, menu_anchor, nullptr); + + return true; +} + +bool Menu::Close() { + // Ensure GTK operations run on the main thread + if (!g_main_context_is_owner(g_main_context_default())) { + struct CloseInvokeData { + Menu* self; + bool result; + GMutex mutex; + GCond cond; + bool done; + } data{this, false}; + + g_mutex_init(&data.mutex); + g_cond_init(&data.cond); + data.done = false; + + g_mutex_lock(&data.mutex); + g_main_context_invoke( + nullptr, + [](gpointer user_data) -> gboolean { + CloseInvokeData* d = static_cast(user_data); + bool r = d->self->Close(); + g_mutex_lock(&d->mutex); + d->result = r; + d->done = true; + g_cond_signal(&d->cond); + g_mutex_unlock(&d->mutex); + return G_SOURCE_REMOVE; + }, + &data); + + while (!data.done) { + g_cond_wait(&data.cond, &data.mutex); + } + g_mutex_unlock(&data.mutex); + g_cond_clear(&data.cond); + g_mutex_clear(&data.mutex); + return data.result; + } + + if (pimpl_->gtk_menu_) { + gtk_menu_popdown(GTK_MENU(pimpl_->gtk_menu_)); + return true; + } + return false; +} + +void* Menu::GetNativeObjectInternal() const { + return (void*)pimpl_->gtk_menu_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/message_dialog_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/message_dialog_linux.cpp new file mode 100644 index 0000000..a308005 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/message_dialog_linux.cpp @@ -0,0 +1,197 @@ +#include "../../dialog.h" +#include "../../message_dialog.h" + +#include + +namespace nativeapi { + +// Private implementation class for MessageDialog +class MessageDialog::Impl { + public: + Impl(const std::string& title, const std::string& message) + : title_(title), message_(message), dialog_(nullptr), is_open_(false) { + // Ensure GTK is initialized + if (!gdk_display_get_default()) { + gtk_init_check(nullptr, nullptr); + } + } + + ~Impl() { + if (dialog_) { + gtk_widget_destroy(dialog_); + dialog_ = nullptr; + } + } + + void SetTitle(const std::string& title) { + title_ = title; + // Title will be applied when dialog is opened + } + + std::string GetTitle() const { return title_; } + + void SetMessage(const std::string& message) { + message_ = message; + // Message will be applied when dialog is opened + } + + std::string GetMessage() const { return message_; } + + bool Open(DialogModality modality) { + // Ensure GTK is initialized + if (!gdk_display_get_default()) { + if (!gtk_init_check(nullptr, nullptr)) { + return false; + } + } + + // For modal dialogs, always create a new dialog since gtk_dialog_run destroys it + // For non-modal dialogs, close existing dialog if open before creating new one + // This ensures title and message are always up to date + if (dialog_ && GTK_IS_WIDGET(dialog_)) { + // Destroy old dialog if exists + gtk_widget_destroy(dialog_); + dialog_ = nullptr; + is_open_ = false; + } + + // Create a new message dialog + dialog_ = gtk_message_dialog_new( + nullptr, // No parent window + GTK_DIALOG_MODAL, // Default to modal (will be overridden for non-modal) + GTK_MESSAGE_INFO, // Message type + GTK_BUTTONS_OK, // Buttons + "%s", // Format string + message_.c_str()); + + if (!dialog_) { + return false; + } + + // Set title + gtk_window_set_title(GTK_WINDOW(dialog_), title_.c_str()); + + // Connect destroy signal to track when dialog is closed by user + g_signal_connect(dialog_, "response", G_CALLBACK(OnResponse), this); + g_signal_connect(dialog_, "destroy", G_CALLBACK(OnDestroy), this); + + // Handle modality + switch (modality) { + case DialogModality::None: + // Non-modal: show the dialog without blocking + gtk_window_set_modal(GTK_WINDOW(dialog_), FALSE); + gtk_widget_show(dialog_); + is_open_ = true; + break; + + case DialogModality::Application: + case DialogModality::Window: + // Modal: block until user responds + gtk_window_set_modal(GTK_WINDOW(dialog_), TRUE); + is_open_ = true; + + // Run the dialog modally - this blocks until user responds + // Note: gtk_dialog_run automatically destroys the dialog when done + gtk_dialog_run(GTK_DIALOG(dialog_)); + + // After gtk_dialog_run returns, the dialog has been dismissed and destroyed + // The OnDestroy callback will set dialog_ to nullptr + is_open_ = false; + break; + } + + return true; + } + + bool Close() { + if (!dialog_ || !is_open_) { + return false; + } + + // Close the dialog programmatically + gtk_dialog_response(GTK_DIALOG(dialog_), GTK_RESPONSE_CLOSE); + + // If it's a modal dialog, we need to destroy it manually + // (gtk_dialog_run already destroyed it) + if (dialog_) { + gtk_widget_destroy(dialog_); + dialog_ = nullptr; + } + + is_open_ = false; + return true; + } + + private: + std::string title_; + std::string message_; + GtkWidget* dialog_; + bool is_open_; + + static void OnResponse(GtkDialog* dialog, gint response_id, gpointer user_data) { + Impl* impl = static_cast(user_data); + + // Mark as closed + impl->is_open_ = false; + + // For non-modal dialogs, destroy the dialog when user responds + if (!gtk_window_get_modal(GTK_WINDOW(dialog))) { + gtk_widget_destroy(GTK_WIDGET(dialog)); + impl->dialog_ = nullptr; + } + } + + static void OnDestroy(GtkWidget* widget, gpointer user_data) { + Impl* impl = static_cast(user_data); + + // Clear the dialog pointer when it's destroyed + if (impl->dialog_ == widget) { + impl->dialog_ = nullptr; + impl->is_open_ = false; + } + } +}; + +// MessageDialog implementation +MessageDialog::MessageDialog(const std::string& title, const std::string& message) + : pimpl_(std::make_unique(title, message)) { + // Set default modality to None (non-modal) + SetModality(DialogModality::None); +} + +MessageDialog::~MessageDialog() = default; + +void MessageDialog::SetTitle(const std::string& title) { + pimpl_->SetTitle(title); +} + +std::string MessageDialog::GetTitle() const { + return pimpl_->GetTitle(); +} + +void MessageDialog::SetMessage(const std::string& message) { + pimpl_->SetMessage(message); +} + +std::string MessageDialog::GetMessage() const { + return pimpl_->GetMessage(); +} + +DialogModality MessageDialog::GetModality() const { + return modality_; +} + +void MessageDialog::SetModality(DialogModality modality) { + modality_ = modality; +} + +bool MessageDialog::Open() { + DialogModality modality = GetModality(); + return pimpl_->Open(modality); +} + +bool MessageDialog::Close() { + return pimpl_->Close(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/preferences_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/preferences_linux.cpp new file mode 100644 index 0000000..7505010 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/preferences_linux.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include +#include "../../preferences.h" + +namespace nativeapi { + +class Preferences::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Use XDG Base Directory Specification + const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); + std::string config_dir; + + if (xdg_config_home) { + config_dir = std::string(xdg_config_home); + } else { + const char* home = getenv("HOME"); + if (!home) { + struct passwd* pw = getpwuid(getuid()); + home = pw->pw_dir; + } + config_dir = std::string(home) + "/.config"; + } + + config_dir += "/nativeapi"; + + // Create directory if it doesn't exist + mkdir(config_dir.c_str(), 0755); + + config_file_ = config_dir + "/preferences_" + scope + ".conf"; + + // Load existing preferences + LoadFromFile(); + } + + ~Impl() { SaveToFile(); } + + bool Set(const std::string& key, const std::string& value) { + data_[key] = value; + return SaveToFile(); + } + + std::string Get(const std::string& key, const std::string& default_value) const { + auto it = data_.find(key); + return (it != data_.end()) ? it->second : default_value; + } + + bool Remove(const std::string& key) { + auto it = data_.find(key); + if (it != data_.end()) { + data_.erase(it); + return SaveToFile(); + } + return false; + } + + bool Clear() { + data_.clear(); + return SaveToFile(); + } + + bool Contains(const std::string& key) const { return data_.find(key) != data_.end(); } + + std::vector GetKeys() const { + std::vector keys; + keys.reserve(data_.size()); + + for (const auto& pair : data_) { + keys.push_back(pair.first); + } + + return keys; + } + + size_t GetSize() const { return data_.size(); } + + std::map GetAll() const { return data_; } + + const std::string& GetScope() const { return scope_; } + + private: + bool LoadFromFile() { + std::ifstream file(config_file_); + if (!file.is_open()) { + return false; + } + + std::string line; + while (std::getline(file, line)) { + // Skip empty lines and comments + if (line.empty() || line[0] == '#') { + continue; + } + + // Parse key=value + size_t pos = line.find('='); + if (pos != std::string::npos) { + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + + // Unescape newlines + size_t escape_pos = 0; + while ((escape_pos = value.find("\\n", escape_pos)) != std::string::npos) { + value.replace(escape_pos, 2, "\n"); + escape_pos += 1; + } + + data_[key] = value; + } + } + + file.close(); + return true; + } + + bool SaveToFile() const { + std::ofstream file(config_file_); + if (!file.is_open()) { + return false; + } + + file << "# NativeAPI Preferences - " << scope_ << std::endl; + + for (const auto& pair : data_) { + std::string value = pair.second; + + // Escape newlines + size_t pos = 0; + while ((pos = value.find('\n', pos)) != std::string::npos) { + value.replace(pos, 1, "\\n"); + pos += 2; + } + + file << pair.first << "=" << value << std::endl; + } + + file.close(); + return true; + } + + std::string scope_; + std::string config_file_; + mutable std::map data_; +}; + +// Constructor implementations +Preferences::Preferences() : Preferences("default") {} + +Preferences::Preferences(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +Preferences::~Preferences() = default; + +// Interface implementation +bool Preferences::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string Preferences::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool Preferences::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool Preferences::Clear() { + return pimpl_->Clear(); +} + +bool Preferences::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector Preferences::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t Preferences::GetSize() const { + return pimpl_->GetSize(); +} + +std::map Preferences::GetAll() const { + return pimpl_->GetAll(); +} + +std::string Preferences::GetScope() const { + return pimpl_->GetScope(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/secure_storage_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/secure_storage_linux.cpp new file mode 100644 index 0000000..dffffc6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/secure_storage_linux.cpp @@ -0,0 +1,107 @@ +#include "../../secure_storage.h" + +namespace nativeapi { + +class SecureStorage::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + std::string GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +SecureStorage::SecureStorage() : SecureStorage("default") {} + +SecureStorage::SecureStorage(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +SecureStorage::~SecureStorage() = default; + +bool SecureStorage::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string SecureStorage::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool SecureStorage::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool SecureStorage::Clear() { + return pimpl_->Clear(); +} + +bool SecureStorage::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector SecureStorage::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t SecureStorage::GetSize() const { + return pimpl_->GetSize(); +} + +std::map SecureStorage::GetAll() const { + return pimpl_->GetAll(); +} + +std::string SecureStorage::GetScope() const { + return pimpl_->GetScope(); +} + +bool SecureStorage::IsAvailable() { + // Stub implementation - report as unavailable + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/shortcut_manager_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/shortcut_manager_linux.cpp new file mode 100644 index 0000000..c5a4ff3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/shortcut_manager_linux.cpp @@ -0,0 +1,397 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../../shortcut_manager.h" + +namespace nativeapi { +namespace { + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + return value; +} + +std::vector SplitAccelerator(const std::string& accelerator) { + std::vector parts; + std::string current; + for (char ch : accelerator) { + if (ch == '+') { + if (!current.empty()) { + parts.push_back(current); + current.clear(); + } + } else if (!std::isspace(static_cast(ch))) { + current.push_back(ch); + } + } + if (!current.empty()) { + parts.push_back(current); + } + return parts; +} + +bool ParseAcceleratorTokens(const std::string& accelerator, + std::vector& modifiers, + std::string& key_token) { + modifiers.clear(); + key_token.clear(); + + auto parts = SplitAccelerator(accelerator); + if (parts.empty()) { + return false; + } + + for (auto& part : parts) { + std::string token = ToLower(part); + if (token == "ctrl" || token == "control" || token == "alt" || token == "option" || + token == "shift" || token == "cmd" || token == "command" || token == "super" || + token == "meta" || token == "cmdorctrl" || token == "commandorcontrol") { + modifiers.push_back(token); + } else { + if (!key_token.empty()) { + return false; + } + key_token = token; + } + } + + return !key_token.empty(); +} + +// Token -> X11 KeySym. +// +// Mirrors the token set in src/shortcut_manager.cpp's validator and the tables +// in the macOS/Windows backends, so the same accelerator string means the same +// key on every platform. +KeySym KeySymFromToken(const std::string& token) { + static const std::unordered_map kKeySyms = { + // Whitespace and editing. + {"space", XK_space}, + {"tab", XK_Tab}, + {"enter", XK_Return}, + {"return", XK_Return}, + {"escape", XK_Escape}, + {"esc", XK_Escape}, + {"backspace", XK_BackSpace}, + {"delete", XK_Delete}, + {"forwarddelete", XK_Delete}, + {"insert", XK_Insert}, + {"help", XK_Help}, + + // Navigation. + {"home", XK_Home}, + {"end", XK_End}, + {"pageup", XK_Page_Up}, + {"pagedown", XK_Page_Down}, + {"up", XK_Up}, + {"down", XK_Down}, + {"left", XK_Left}, + {"right", XK_Right}, + + // Punctuation, by name and by literal character. + {"plus", XK_plus}, + {"equal", XK_equal}, {"=", XK_equal}, + {"minus", XK_minus}, {"-", XK_minus}, + {"comma", XK_comma}, {",", XK_comma}, + {"period", XK_period}, {".", XK_period}, + {"slash", XK_slash}, {"/", XK_slash}, + {"backslash", XK_backslash}, {"\\", XK_backslash}, + {"semicolon", XK_semicolon}, {";", XK_semicolon}, + {"quote", XK_apostrophe}, {"'", XK_apostrophe}, + {"leftbracket", XK_bracketleft}, {"[", XK_bracketleft}, + {"rightbracket", XK_bracketright}, {"]", XK_bracketright}, + {"grave", XK_grave}, {"backquote", XK_grave}, {"`", XK_grave}, + + // Keypad. + {"num0", XK_KP_0}, {"num1", XK_KP_1}, {"num2", XK_KP_2}, + {"num3", XK_KP_3}, {"num4", XK_KP_4}, {"num5", XK_KP_5}, + {"num6", XK_KP_6}, {"num7", XK_KP_7}, {"num8", XK_KP_8}, + {"num9", XK_KP_9}, + {"numdec", XK_KP_Decimal}, + {"numadd", XK_KP_Add}, + {"numsub", XK_KP_Subtract}, + {"nummult", XK_KP_Multiply}, + {"numdiv", XK_KP_Divide}, + {"numenter", XK_KP_Enter}, + }; + + // Letters and digits resolve through Xlib's own name table. + if (token.size() == 1) { + char ch = token[0]; + if (std::isalpha(static_cast(ch))) { + return XStringToKeysym(std::string(1, static_cast(std::toupper(ch))).c_str()); + } + if (std::isdigit(static_cast(ch))) { + return XStringToKeysym(std::string(1, ch).c_str()); + } + } + + // Function keys. XK_F1..XK_F24 are contiguous. + if (token.size() > 1 && token[0] == 'f' && + token.find_first_not_of("0123456789", 1) == std::string::npos) { + int fnum = std::stoi(token.substr(1)); + if (fnum >= 1 && fnum <= 24) { + return XK_F1 + (fnum - 1); + } + return NoSymbol; + } + + auto it = kKeySyms.find(token); + return it == kKeySyms.end() ? NoSymbol : it->second; +} + +bool ParseAcceleratorLinux(const std::string& accelerator, + unsigned int& modifiers, + KeyCode& keycode, + ::Display* display) { + modifiers = 0; + keycode = 0; + + std::vector modifier_tokens; + std::string key_token; + if (!ParseAcceleratorTokens(accelerator, modifier_tokens, key_token)) { + return false; + } + + for (const auto& token : modifier_tokens) { + if (token == "ctrl" || token == "control" || token == "cmdorctrl" || + token == "commandorcontrol") { + modifiers |= ControlMask; + } else if (token == "alt" || token == "option") { + modifiers |= Mod1Mask; + } else if (token == "shift") { + modifiers |= ShiftMask; + } else if (token == "cmd" || token == "command" || token == "super" || token == "meta") { + modifiers |= Mod4Mask; + } + } + + KeySym keysym = KeySymFromToken(key_token); + if (keysym == NoSymbol) { + return false; + } + + keycode = XKeysymToKeycode(display, keysym); + return keycode != 0; +} + +} // namespace + +class ShortcutManagerImpl final : public ShortcutManager::Impl { + public: + explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) { + XInitThreads(); + display_ = XOpenDisplay(nullptr); + if (display_) { + root_ = DefaultRootWindow(display_); + exit_atom_ = XInternAtom(display_, "NATIVEAPI_SHORTCUT_EXIT", False); + } + } + + ~ShortcutManagerImpl() override { + StopThread(); + if (display_) { + XCloseDisplay(display_); + display_ = nullptr; + } + } + + bool IsSupported() override { return display_ != nullptr; } + + bool RegisterShortcut(const std::shared_ptr& shortcut) override { + if (!display_) { + return false; + } + + unsigned int modifiers = 0; + KeyCode keycode = 0; + if (!ParseAcceleratorLinux(shortcut->GetAccelerator(), modifiers, keycode, display_)) { + return false; + } + + GrabKeyWithModifiers(keycode, modifiers); + + { + std::lock_guard lock(mutex_); + GrabInfo info{keycode, modifiers}; + grabs_[shortcut->GetId()] = info; + combo_to_shortcut_[ComposeKey(modifiers, keycode)] = shortcut->GetId(); + } + + EnsureThread(); + return true; + } + + bool UnregisterShortcut(const std::shared_ptr& shortcut) override { + if (!display_) { + return false; + } + + GrabInfo info; + { + std::lock_guard lock(mutex_); + auto it = grabs_.find(shortcut->GetId()); + if (it == grabs_.end()) { + return false; + } + info = it->second; + grabs_.erase(it); + combo_to_shortcut_.erase(ComposeKey(info.modifiers, info.keycode)); + } + + UngrabKeyWithModifiers(info.keycode, info.modifiers); + return true; + } + + void SetupEventMonitoring() override { EnsureThread(); } + + void CleanupEventMonitoring() override { + // Keep thread running while shortcuts may still be registered. + } + + private: + struct GrabInfo { + KeyCode keycode; + unsigned int modifiers; + }; + + void GrabKeyWithModifiers(KeyCode keycode, unsigned int modifiers) { + const unsigned int extra_masks[] = {0, LockMask, Mod2Mask, LockMask | Mod2Mask}; + for (unsigned int mask : extra_masks) { + XGrabKey(display_, keycode, modifiers | mask, root_, True, GrabModeAsync, GrabModeAsync); + } + XSync(display_, False); + } + + void UngrabKeyWithModifiers(KeyCode keycode, unsigned int modifiers) { + const unsigned int extra_masks[] = {0, LockMask, Mod2Mask, LockMask | Mod2Mask}; + for (unsigned int mask : extra_masks) { + XUngrabKey(display_, keycode, modifiers | mask, root_); + } + XSync(display_, False); + } + + uint32_t ComposeKey(unsigned int modifiers, KeyCode keycode) const { + return (static_cast(modifiers & 0xFFFF) << 16) | (keycode & 0xFFFF); + } + + void EnsureThread() { + if (running_.load() || !display_) { + return; + } + + running_.store(true); + event_thread_ = std::thread([this]() { ThreadMain(); }); + } + + void StopThread() { + if (!running_.load()) { + return; + } + + running_.store(false); + SendExitMessage(); + if (event_thread_.joinable()) { + event_thread_.join(); + } + } + + void SendExitMessage() { + if (!display_) { + return; + } + + XClientMessageEvent client_message = {}; + client_message.type = ClientMessage; + client_message.message_type = exit_atom_; + client_message.window = root_; + client_message.format = 32; + XSendEvent(display_, root_, False, 0, reinterpret_cast(&client_message)); + XFlush(display_); + } + + void ThreadMain() { + if (!display_) { + return; + } + + XSelectInput(display_, root_, KeyPressMask); + + while (running_.load()) { + XEvent event; + XNextEvent(display_, &event); + + if (!running_.load()) { + break; + } + + if (event.type == ClientMessage) { + if (event.xclient.message_type == exit_atom_) { + break; + } + } + + if (event.type != KeyPress) { + continue; + } + + unsigned int normalized_mods = event.xkey.state & ~(LockMask | Mod2Mask); + uint32_t combo = ComposeKey(normalized_mods, event.xkey.keycode); + + ShortcutId shortcut_id = 0; + { + std::lock_guard lock(mutex_); + auto it = combo_to_shortcut_.find(combo); + if (it == combo_to_shortcut_.end()) { + continue; + } + shortcut_id = it->second; + } + + auto shortcut = manager_->Get(shortcut_id); + if (!shortcut) { + continue; + } + + if (!manager_->IsEnabled() || !shortcut->IsEnabled()) { + continue; + } + + manager_->EmitShortcutActivated(shortcut_id, shortcut->GetAccelerator()); + shortcut->Invoke(); + } + } + + ShortcutManager* manager_; + // ::-qualified: nativeapi::Display/Window (via id_allocator.h) shadow the + // X11 typedefs inside this namespace. + ::Display* display_ = nullptr; + ::Window root_ = 0; + Atom exit_atom_ = None; + + std::mutex mutex_; + std::unordered_map grabs_; + std::unordered_map combo_to_shortcut_; + + std::atomic running_{false}; + std::thread event_thread_; +}; + +ShortcutManager::ShortcutManager() + : pimpl_(std::make_unique(this)), next_shortcut_id_(1), enabled_(true) {} + +ShortcutManager::~ShortcutManager() { + UnregisterAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/tray_icon_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/tray_icon_linux.cpp new file mode 100644 index 0000000..5950dee --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/tray_icon_linux.cpp @@ -0,0 +1,914 @@ +// Linux tray icon implemented via the StatusNotifierItem D-Bus specification. +// https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ +// +// Uses GDBus (part of GLib/GIO, already a transitive dependency of GTK) so that +// no GPL-licensed libayatana-appindicator dependency is required. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../foundation/id_allocator.h" +#include "../../image.h" +#include "../../menu.h" +#include "../../tray_icon.h" + +namespace nativeapi { + +// ── D-Bus introspection XML for org.kde.StatusNotifierItem ─────────────────── + +static const char kSniIntrospectionXml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +static const char kDbusMenuIntrospectionXml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +// ── Icon pixel-data conversion ─────────────────────────────────────────────── + +// Returns a GVariant of type a(iiay) containing one entry for the supplied +// pixbuf (or an empty array when pixbuf is nullptr). Each pixel is encoded as +// four bytes in network byte order: Alpha, Red, Green, Blue (ARGB32). +static GVariant* PixbufToSniIconPixmaps(GdkPixbuf* pixbuf) { + GVariantBuilder array_builder; + g_variant_builder_init(&array_builder, G_VARIANT_TYPE("a(iiay)")); + + if (pixbuf) { + const int width = gdk_pixbuf_get_width(pixbuf); + const int height = gdk_pixbuf_get_height(pixbuf); + const int rowstride = gdk_pixbuf_get_rowstride(pixbuf); + const int n_channels = gdk_pixbuf_get_n_channels(pixbuf); + const gboolean has_alpha = gdk_pixbuf_get_has_alpha(pixbuf); + const guchar* pixels = gdk_pixbuf_get_pixels(pixbuf); + + std::vector argb; + argb.reserve(static_cast(width * height * 4)); + + for (int row = 0; row < height; ++row) { + const guchar* p = pixels + row * rowstride; + for (int col = 0; col < width; ++col) { + const uint8_t r = p[0]; + const uint8_t g = p[1]; + const uint8_t b = p[2]; + const uint8_t a = has_alpha ? p[3] : 255u; + argb.push_back(a); + argb.push_back(r); + argb.push_back(g); + argb.push_back(b); + p += n_channels; + } + } + + GVariantBuilder entry_builder; + g_variant_builder_init(&entry_builder, G_VARIANT_TYPE("(iiay)")); + g_variant_builder_add(&entry_builder, "i", width); + g_variant_builder_add(&entry_builder, "i", height); + g_variant_builder_add_value( + &entry_builder, + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, argb.data(), argb.size(), sizeof(uint8_t))); + g_variant_builder_add_value(&array_builder, g_variant_builder_end(&entry_builder)); + } + + return g_variant_builder_end(&array_builder); +} + +// ── Private implementation ─────────────────────────────────────────────────── + +class TrayIcon::Impl { + public: + TrayIcon* owner_; + TrayIconId id_; + + std::shared_ptr image_; + std::optional title_; + std::optional tooltip_; + std::shared_ptr context_menu_; + bool visible_; + ContextMenuTrigger context_menu_trigger_; + + // D-Bus state + GDBusConnection* connection_; + guint registration_id_; + guint menu_registration_id_; + guint name_owner_id_; + std::string service_name_; + std::unordered_map dbusmenu_items_; + unsigned int menu_revision_; + + explicit Impl(TrayIcon* owner) + : owner_(owner), + image_(nullptr), + title_(std::nullopt), + tooltip_(std::nullopt), + context_menu_(nullptr), + visible_(false), + context_menu_trigger_(ContextMenuTrigger::None), + connection_(nullptr), + registration_id_(0), + menu_registration_id_(0), + name_owner_id_(0), + menu_revision_(1) { + id_ = IdAllocator::Allocate(); + } + + ~Impl() { Cleanup(); } + + // Connect to the session bus, register the SNI object, and request a + // well-known service name. Returns false on error (icon will be invisible). + bool Init() { + GError* error = nullptr; + connection_ = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); + if (!connection_) { + if (error) { + std::cerr << "[nativeapi] SNI: D-Bus session connection failed: " << error->message + << std::endl; + g_error_free(error); + } + return false; + } + + static std::atomic next_sni_index{1}; + service_name_ = "org.kde.StatusNotifierItem-" + + std::to_string(static_cast(getpid())) + "-" + + std::to_string(next_sni_index++); + + GDBusNodeInfo* node_info = g_dbus_node_info_new_for_xml(kSniIntrospectionXml, &error); + if (!node_info) { + if (error) { + std::cerr << "[nativeapi] SNI: Bad introspection XML: " << error->message << std::endl; + g_error_free(error); + } + return false; + } + + GDBusInterfaceInfo* iface_info = + g_dbus_node_info_lookup_interface(node_info, "org.kde.StatusNotifierItem"); + + static const GDBusInterfaceVTable vtable = { + &Impl::OnMethodCall, + &Impl::OnGetProperty, + nullptr // no writable properties + }; + + registration_id_ = g_dbus_connection_register_object(connection_, "/StatusNotifierItem", + iface_info, &vtable, + this, // user_data + nullptr, // user_data_free_func + &error); + g_dbus_node_info_unref(node_info); + + if (registration_id_ == 0) { + if (error) { + std::cerr << "[nativeapi] SNI: Object registration failed: " << error->message + << std::endl; + g_error_free(error); + } + return false; + } + + GDBusNodeInfo* menu_node_info = + g_dbus_node_info_new_for_xml(kDbusMenuIntrospectionXml, &error); + if (!menu_node_info) { + if (error) { + std::cerr << "[nativeapi] SNI: Bad dbusmenu introspection XML: " << error->message + << std::endl; + g_error_free(error); + } + return false; + } + + GDBusInterfaceInfo* menu_iface_info = + g_dbus_node_info_lookup_interface(menu_node_info, "com.canonical.dbusmenu"); + + static const GDBusInterfaceVTable menu_vtable = { + &Impl::OnMenuMethodCall, + &Impl::OnMenuGetProperty, + nullptr // no writable properties + }; + + menu_registration_id_ = g_dbus_connection_register_object( + connection_, "/StatusNotifierItem/Menu", menu_iface_info, &menu_vtable, this, nullptr, + &error); + g_dbus_node_info_unref(menu_node_info); + + if (menu_registration_id_ == 0) { + if (error) { + std::cerr << "[nativeapi] SNI: dbusmenu object registration failed: " << error->message + << std::endl; + g_error_free(error); + } + return false; + } + + name_owner_id_ = g_bus_own_name_on_connection(connection_, service_name_.c_str(), + G_BUS_NAME_OWNER_FLAGS_NONE, &Impl::OnNameAcquired, + &Impl::OnNameLost, this, nullptr); + return true; + } + + void Cleanup() { + // Nullify the back-pointer first so any in-flight GLib callbacks that + // haven't been dispatched yet will see a null owner and skip event emission. + owner_ = nullptr; + + if (name_owner_id_ != 0) { + g_bus_unown_name(name_owner_id_); + name_owner_id_ = 0; + } + if (connection_ && registration_id_ != 0) { + g_dbus_connection_unregister_object(connection_, registration_id_); + registration_id_ = 0; + } + if (connection_ && menu_registration_id_ != 0) { + g_dbus_connection_unregister_object(connection_, menu_registration_id_); + menu_registration_id_ = 0; + } + if (connection_) { + g_object_unref(connection_); + connection_ = nullptr; + } + } + + void EmitSignal(const char* signal_name, GVariant* params = nullptr) { + if (!connection_ || registration_id_ == 0) return; + GError* error = nullptr; + g_dbus_connection_emit_signal(connection_, nullptr, "/StatusNotifierItem", + "org.kde.StatusNotifierItem", signal_name, params, &error); + if (error) g_error_free(error); + } + + bool ShouldExposeMenu() const { + return context_menu_ != nullptr && context_menu_trigger_ == ContextMenuTrigger::Clicked; + } + + void EmitMenuPropertiesChanged() { + if (!connection_ || registration_id_ == 0) return; + + const bool expose_menu = ShouldExposeMenu(); + + GVariantBuilder changed; + GVariantBuilder invalidated; + g_variant_builder_init(&changed, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_init(&invalidated, G_VARIANT_TYPE("as")); + g_variant_builder_add(&changed, "{sv}", "ItemIsMenu", g_variant_new_boolean(expose_menu)); + g_variant_builder_add(&changed, "{sv}", "Menu", + g_variant_new_object_path(expose_menu ? "/StatusNotifierItem/Menu" + : "/")); + + GError* error = nullptr; + g_dbus_connection_emit_signal( + connection_, nullptr, "/StatusNotifierItem", "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(s@a{sv}@as)", "org.kde.StatusNotifierItem", + g_variant_builder_end(&changed), g_variant_builder_end(&invalidated)), + &error); + if (error) g_error_free(error); + } + + // ── D-Bus name callbacks ────────────────────────────────────────────────── + + static void OnNameAcquired(GDBusConnection* conn, const gchar* name, gpointer user_data) { + Impl* self = static_cast(user_data); + if (self) self->RegisterWithWatcher(conn, name); + } + + static void OnNameLost(GDBusConnection*, const gchar* name, gpointer) { + std::cerr << "[nativeapi] SNI: lost D-Bus name " << (name ? name : "(null)") << std::endl; + } + + // Try both KDE and Canonical watcher service names. + void RegisterWithWatcher(GDBusConnection* conn, const gchar* service_name) { + static const char* const kWatchers[] = { + "org.kde.StatusNotifierWatcher", + "com.canonical.StatusNotifierWatcher", + nullptr, + }; + for (int i = 0; kWatchers[i]; ++i) { + GError* error = nullptr; + GVariant* reply = g_dbus_connection_call_sync( + conn, kWatchers[i], "/StatusNotifierWatcher", kWatchers[i], + "RegisterStatusNotifierItem", g_variant_new("(s)", service_name), nullptr, + G_DBUS_CALL_FLAGS_NONE, 2000, nullptr, &error); + if (reply) { + g_variant_unref(reply); + return; + } + if (error) g_error_free(error); + } + std::cerr << "[nativeapi] SNI: no StatusNotifierWatcher found; tray icon may not appear" + << std::endl; + } + + // ── D-Bus method-call handler ───────────────────────────────────────────── + + static void OnMethodCall(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* method_name, GVariant*, + GDBusMethodInvocation* invocation, gpointer user_data) { + if (!user_data) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Internal error"); + return; + } + + if (g_strcmp0(method_name, "Activate") == 0 || + g_strcmp0(method_name, "SecondaryActivate") == 0 || + g_strcmp0(method_name, "ContextMenu") == 0 || + g_strcmp0(method_name, "Scroll") == 0) { + g_dbus_method_invocation_return_value(invocation, nullptr); + } else { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method: %s", method_name); + } + } + + // ── D-Bus property getter ───────────────────────────────────────────────── + + static GVariant* OnGetProperty(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* property_name, GError** error, + gpointer user_data) { + Impl* self = static_cast(user_data); + if (!self) return nullptr; + + if (g_strcmp0(property_name, "Category") == 0) + return g_variant_new_string("ApplicationStatus"); + + if (g_strcmp0(property_name, "Id") == 0) + return g_variant_new_string("nativeapi-tray"); + + if (g_strcmp0(property_name, "Title") == 0) + return g_variant_new_string(self->title_.value_or("").c_str()); + + if (g_strcmp0(property_name, "Status") == 0) + return g_variant_new_string(self->visible_ ? "Active" : "Passive"); + + if (g_strcmp0(property_name, "WindowId") == 0) + return g_variant_new_uint32(0); + + if (g_strcmp0(property_name, "IconName") == 0) + return g_variant_new_string(""); // we use IconPixmap instead + + if (g_strcmp0(property_name, "IconPixmap") == 0) { + // image_linux.cpp's GetNativeObjectInternal() returns GdkPixbuf* on Linux. + GdkPixbuf* pb = + self->image_ ? static_cast(self->image_->GetNativeObject()) : nullptr; + return PixbufToSniIconPixmaps(pb); + } + + if (g_strcmp0(property_name, "OverlayIconName") == 0) return g_variant_new_string(""); + if (g_strcmp0(property_name, "OverlayIconPixmap") == 0) return PixbufToSniIconPixmaps(nullptr); + if (g_strcmp0(property_name, "AttentionIconName") == 0) return g_variant_new_string(""); + if (g_strcmp0(property_name, "AttentionIconPixmap") == 0) + return PixbufToSniIconPixmaps(nullptr); + if (g_strcmp0(property_name, "AttentionMovieName") == 0) return g_variant_new_string(""); + + if (g_strcmp0(property_name, "ToolTip") == 0) { + // (sa(iiay)ss): iconName, iconPixmap[], title, description + const std::string& tip = self->tooltip_.value_or(self->title_.value_or("")); + return g_variant_new("(s@a(iiay)ss)", "", PixbufToSniIconPixmaps(nullptr), + self->title_.value_or("").c_str(), tip.c_str()); + } + + if (g_strcmp0(property_name, "ItemIsMenu") == 0) + return g_variant_new_boolean(self->ShouldExposeMenu()); + if (g_strcmp0(property_name, "Menu") == 0) { + const bool expose_menu = self->ShouldExposeMenu(); + return g_variant_new_object_path(expose_menu ? "/StatusNotifierItem/Menu" : "/"); + } + + if (error) { + *error = g_error_new(G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, + "Unknown property: %s", property_name); + } + return nullptr; + } + + static std::string GtkMenuItemLabel(GtkWidget* item) { + if (!item || !GTK_IS_MENU_ITEM(item) || GTK_IS_SEPARATOR_MENU_ITEM(item)) { + return ""; + } + + const char* label = gtk_menu_item_get_label(GTK_MENU_ITEM(item)); + if (label && label[0] != '\0') { + return label; + } + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(item)); + if (!child) { + return ""; + } + + if (GTK_IS_LABEL(child)) { + label = gtk_label_get_text(GTK_LABEL(child)); + return label ? label : ""; + } + + if (GTK_IS_CONTAINER(child)) { + GList* children = gtk_container_get_children(GTK_CONTAINER(child)); + for (GList* iter = children; iter; iter = iter->next) { + GtkWidget* widget = GTK_WIDGET(iter->data); + if (GTK_IS_LABEL(widget)) { + label = gtk_label_get_text(GTK_LABEL(widget)); + std::string result = label ? label : ""; + g_list_free(children); + return result; + } + } + g_list_free(children); + } + + return ""; + } + + int RegisterDbusMenuItem(GtkWidget* item) { + int id = static_cast(reinterpret_cast(item) & 0x7fffffff); + if (id == 0) { + id = 1; + } + dbusmenu_items_[id] = item; + return id; + } + + void AppendMenuItemProperties(GVariantBuilder* props, GtkWidget* item) { + const bool is_separator = item && GTK_IS_SEPARATOR_MENU_ITEM(item); + const bool enabled = item ? gtk_widget_get_sensitive(item) == TRUE : true; + const bool visible = true; + + g_variant_builder_add(props, "{sv}", "enabled", g_variant_new_boolean(enabled)); + g_variant_builder_add(props, "{sv}", "visible", g_variant_new_boolean(visible)); + + if (is_separator) { + g_variant_builder_add(props, "{sv}", "type", g_variant_new_string("separator")); + return; + } + + std::string label = GtkMenuItemLabel(item); + if (!label.empty()) { + g_variant_builder_add(props, "{sv}", "label", g_variant_new_string(label.c_str())); + } + + if (item && GTK_IS_CHECK_MENU_ITEM(item)) { + const bool is_radio = GTK_IS_RADIO_MENU_ITEM(item); + g_variant_builder_add(props, "{sv}", "toggle-type", + g_variant_new_string(is_radio ? "radio" : "checkmark")); + g_variant_builder_add( + props, "{sv}", "toggle-state", + g_variant_new_int32(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item)) ? 1 : 0)); + } + + if (item && GTK_IS_MENU_ITEM(item) && gtk_menu_item_get_submenu(GTK_MENU_ITEM(item))) { + g_variant_builder_add(props, "{sv}", "children-display", g_variant_new_string("submenu")); + } + } + + GVariant* BuildDbusMenuLayout(GtkWidget* menu, int item_id = 0) { + GVariantBuilder props; + GVariantBuilder children; + g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_init(&children, G_VARIANT_TYPE("av")); + + if (item_id != 0 && menu && GTK_IS_MENU_ITEM(menu)) { + AppendMenuItemProperties(&props, menu); + GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu)); + if (submenu && GTK_IS_MENU_SHELL(submenu)) { + GList* items = gtk_container_get_children(GTK_CONTAINER(submenu)); + for (GList* iter = items; iter; iter = iter->next) { + GtkWidget* child = GTK_WIDGET(iter->data); + int child_id = RegisterDbusMenuItem(child); + g_variant_builder_add(&children, "v", BuildDbusMenuLayout(child, child_id)); + } + g_list_free(items); + } + } else if (menu && GTK_IS_MENU_SHELL(menu)) { + dbusmenu_items_.clear(); + GList* items = gtk_container_get_children(GTK_CONTAINER(menu)); + for (GList* iter = items; iter; iter = iter->next) { + GtkWidget* child = GTK_WIDGET(iter->data); + int child_id = RegisterDbusMenuItem(child); + g_variant_builder_add(&children, "v", BuildDbusMenuLayout(child, child_id)); + } + g_list_free(items); + } + + return g_variant_new("(i@a{sv}@av)", item_id, g_variant_builder_end(&props), + g_variant_builder_end(&children)); + } + + GVariant* BuildDbusMenuProperties(int id) { + GVariantBuilder props; + g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); + auto it = dbusmenu_items_.find(id); + if (it != dbusmenu_items_.end()) { + AppendMenuItemProperties(&props, it->second); + } + return g_variant_new("(i@a{sv})", id, g_variant_builder_end(&props)); + } + + void ActivateDbusMenuItem(int id) { + auto it = dbusmenu_items_.find(id); + if (it == dbusmenu_items_.end() || !it->second || !GTK_IS_MENU_ITEM(it->second)) { + return; + } + gtk_menu_item_activate(GTK_MENU_ITEM(it->second)); + } + + static void OnMenuMethodCall(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* method_name, GVariant* parameters, + GDBusMethodInvocation* invocation, gpointer user_data) { + Impl* self = static_cast(user_data); + if (!self) { + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Internal error"); + return; + } + + if (g_strcmp0(method_name, "GetLayout") == 0) { + gint parent_id = 0; + if (parameters) { + gint recursion_depth = -1; + GVariantIter* property_names = nullptr; + g_variant_get(parameters, "(iias)", &parent_id, &recursion_depth, &property_names); + if (property_names) { + g_variant_iter_free(property_names); + } + } + + GtkWidget* menu = self->context_menu_ + ? static_cast(self->context_menu_->GetNativeObject()) + : nullptr; + if (parent_id != 0) { + auto it = self->dbusmenu_items_.find(parent_id); + menu = it != self->dbusmenu_items_.end() ? it->second : nullptr; + } + g_dbus_method_invocation_return_value( + invocation, + g_variant_new("(u@(ia{sv}av))", self->menu_revision_, + self->BuildDbusMenuLayout(menu, parent_id))); + return; + } + + if (g_strcmp0(method_name, "GetGroupProperties") == 0) { + GVariantIter* ids = nullptr; + GVariantIter* property_names = nullptr; + if (parameters) { + g_variant_get(parameters, "(aias)", &ids, &property_names); + } + + GVariantBuilder result; + g_variant_builder_init(&result, G_VARIANT_TYPE("a(ia{sv})")); + if (ids) { + gint item_id = 0; + while (g_variant_iter_loop(ids, "i", &item_id)) { + g_variant_builder_add_value(&result, self->BuildDbusMenuProperties(item_id)); + } + g_variant_iter_free(ids); + } + if (property_names) { + g_variant_iter_free(property_names); + } + g_dbus_method_invocation_return_value(invocation, + g_variant_new("(@a(ia{sv}))", + g_variant_builder_end(&result))); + return; + } + + if (g_strcmp0(method_name, "GetProperty") == 0) { + gint item_id = 0; + const gchar* name = nullptr; + if (parameters) { + g_variant_get(parameters, "(i&s)", &item_id, &name); + } + + GVariantBuilder props; + g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); + auto it = self->dbusmenu_items_.find(item_id); + if (it != self->dbusmenu_items_.end()) { + self->AppendMenuItemProperties(&props, it->second); + } + GVariant* props_variant = g_variant_builder_end(&props); + GVariant* value = g_variant_lookup_value(props_variant, name ? name : "", nullptr); + g_variant_unref(props_variant); + g_dbus_method_invocation_return_value( + invocation, + g_variant_new("(@v)", value ? g_variant_new_variant(value) + : g_variant_new_variant(g_variant_new_string("")))); + if (value) { + g_variant_unref(value); + } + return; + } + + if (g_strcmp0(method_name, "Event") == 0) { + gint item_id = 0; + const gchar* event_id = nullptr; + GVariant* data = nullptr; + guint timestamp = 0; + if (parameters) { + g_variant_get(parameters, "(i&svu)", &item_id, &event_id, &data, ×tamp); + if (data) { + g_variant_unref(data); + } + } + if (event_id && (g_strcmp0(event_id, "clicked") == 0 || + g_strcmp0(event_id, "activated") == 0)) { + self->ActivateDbusMenuItem(item_id); + } + g_dbus_method_invocation_return_value(invocation, nullptr); + return; + } + + if (g_strcmp0(method_name, "EventGroup") == 0) { + GVariantIter* events = nullptr; + if (parameters) { + g_variant_get(parameters, "(a(isvu))", &events); + } + if (events) { + gint item_id = 0; + const gchar* event_id = nullptr; + GVariant* data = nullptr; + guint timestamp = 0; + while (g_variant_iter_loop(events, "(i&svu)", &item_id, &event_id, &data, ×tamp)) { + if (event_id && (g_strcmp0(event_id, "clicked") == 0 || + g_strcmp0(event_id, "activated") == 0)) { + self->ActivateDbusMenuItem(item_id); + } + } + g_variant_iter_free(events); + } + + GVariantBuilder errors; + g_variant_builder_init(&errors, G_VARIANT_TYPE("ai")); + g_dbus_method_invocation_return_value(invocation, + g_variant_new("(@ai)", + g_variant_builder_end(&errors))); + return; + } + + if (g_strcmp0(method_name, "AboutToShow") == 0) { + g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", FALSE)); + return; + } + + if (g_strcmp0(method_name, "AboutToShowGroup") == 0) { + GVariantBuilder updates; + GVariantBuilder errors; + g_variant_builder_init(&updates, G_VARIANT_TYPE("ai")); + g_variant_builder_init(&errors, G_VARIANT_TYPE("ai")); + g_dbus_method_invocation_return_value( + invocation, g_variant_new("(@ai@ai)", g_variant_builder_end(&updates), + g_variant_builder_end(&errors))); + return; + } + + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown dbusmenu method: %s", method_name); + } + + static GVariant* OnMenuGetProperty(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* property_name, GError** error, + gpointer user_data) { + if (g_strcmp0(property_name, "Version") == 0) return g_variant_new_uint32(3); + if (g_strcmp0(property_name, "TextDirection") == 0) return g_variant_new_string("ltr"); + if (g_strcmp0(property_name, "Status") == 0) return g_variant_new_string("normal"); + if (g_strcmp0(property_name, "IconThemePath") == 0) { + GVariantBuilder paths; + g_variant_builder_init(&paths, G_VARIANT_TYPE("as")); + return g_variant_builder_end(&paths); + } + + if (error) { + *error = g_error_new(G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, + "Unknown dbusmenu property: %s", property_name); + } + return nullptr; + } +}; + +// ── TrayIcon public interface ───────────────────────────────────────────────── + +TrayIcon::TrayIcon() : pimpl_(std::make_unique(this)) { + if (pimpl_->Init()) { + pimpl_->visible_ = true; + } else { + std::cerr << "[nativeapi] TrayIcon: D-Bus initialisation failed; icon will not appear" + << std::endl; + } +} + +TrayIcon::TrayIcon(void* /*tray*/) : pimpl_(std::make_unique(this)) { + // For API compatibility; create a fresh SNI tray icon ignoring the raw pointer. + if (pimpl_->Init()) { + pimpl_->visible_ = true; + } else { + std::cerr << "[nativeapi] TrayIcon: D-Bus initialisation failed; icon will not appear" + << std::endl; + } +} + +TrayIcon::~TrayIcon() { + // Impl::~Impl calls Cleanup(), which unregisters the D-Bus object and + // releases the connection before pimpl_ is destroyed. +} + +TrayIconId TrayIcon::GetId() { + return pimpl_->id_; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + pimpl_->image_ = image; + pimpl_->EmitSignal("NewIcon"); +} + +std::shared_ptr TrayIcon::GetIcon() const { + return pimpl_->image_; +} + +void TrayIcon::SetTitle(std::optional title) { + pimpl_->title_ = title; + pimpl_->EmitSignal("NewTitle"); +} + +std::optional TrayIcon::GetTitle() { + return pimpl_->title_; +} + +void TrayIcon::SetTooltip(std::optional tooltip) { + pimpl_->tooltip_ = tooltip; + pimpl_->EmitSignal("NewToolTip"); +} + +std::optional TrayIcon::GetTooltip() { + return pimpl_->tooltip_; +} + +void TrayIcon::SetContextMenu(std::shared_ptr menu) { + pimpl_->context_menu_ = menu; + ++pimpl_->menu_revision_; + pimpl_->EmitSignal("NewStatus", + g_variant_new("(s)", pimpl_->visible_ ? "Active" : "Passive")); + pimpl_->EmitMenuPropertiesChanged(); + if (pimpl_->connection_ && pimpl_->menu_registration_id_ != 0) { + g_dbus_connection_emit_signal(pimpl_->connection_, nullptr, "/StatusNotifierItem/Menu", + "com.canonical.dbusmenu", "LayoutUpdated", + g_variant_new("(ui)", pimpl_->menu_revision_, 0), nullptr); + } +} + +std::shared_ptr TrayIcon::GetContextMenu() { + return pimpl_->context_menu_; +} + +Rectangle TrayIcon::GetBounds() { + // The SNI specification does not expose icon geometry; return empty bounds. + return {0, 0, 0, 0}; +} + +bool TrayIcon::SetVisible(bool visible) { + pimpl_->visible_ = visible; + const char* status = visible ? "Active" : "Passive"; + pimpl_->EmitSignal("NewStatus", g_variant_new("(s)", status)); + return true; +} + +bool TrayIcon::IsVisible() { + return pimpl_->visible_; +} + +bool TrayIcon::OpenContextMenu() { + return false; +} + +bool TrayIcon::CloseContextMenu() { + return true; +} + +void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { + pimpl_->context_menu_trigger_ = trigger; + pimpl_->EmitSignal("NewStatus", + g_variant_new("(s)", pimpl_->visible_ ? "Active" : "Passive")); + pimpl_->EmitMenuPropertiesChanged(); +} + +ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { + return pimpl_->context_menu_trigger_; +} + +void* TrayIcon::GetNativeObjectInternal() const { + return static_cast(pimpl_->connection_); +} + +void TrayIcon::StartEventListening() { + // GDBus dispatches D-Bus method calls via the GLib main loop automatically. +} + +void TrayIcon::StopEventListening() { + // Nothing to tear down; GDBus uses the GLib main loop. +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/tray_manager_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/tray_manager_linux.cpp new file mode 100644 index 0000000..69edef8 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/tray_manager_linux.cpp @@ -0,0 +1,58 @@ +#include +#include +#include + +#include "../../tray_icon.h" +#include "../../tray_manager.h" + +namespace nativeapi { + +class TrayManager::Impl { + public: + Impl() {} + ~Impl() {} +}; + +TrayManager::TrayManager() : next_tray_id_(1), pimpl_(std::make_unique()) {} + +TrayManager::~TrayManager() { + std::lock_guard lock(mutex_); + trays_.clear(); +} + +bool TrayManager::IsSupported() { + // Cache the result: session bus availability does not change during runtime. + static bool checked = false; + static bool supported = false; + if (!checked) { + GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); + if (conn) { + g_object_unref(conn); + supported = true; + } + checked = true; + } + return supported; +} + +std::shared_ptr TrayManager::Get(TrayIconId id) { + std::lock_guard lock(mutex_); + + auto it = trays_.find(id); + if (it != trays_.end()) { + return it->second; + } + return nullptr; +} + +std::vector> TrayManager::GetAll() { + std::lock_guard lock(mutex_); + + std::vector> trays; + for (const auto& pair : trays_) { + trays.push_back(pair.second); + } + return trays; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/url_opener_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/url_opener_linux.cpp new file mode 100644 index 0000000..eb2f776 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/url_opener_linux.cpp @@ -0,0 +1,45 @@ +#include + +#include + +#include "../../url_opener.h" + +namespace nativeapi { +namespace { + +class LinuxUrlOpenerImpl final : public UrlOpener::Impl { + public: + bool IsSupported() const override { return true; } + + UrlOpenResult Open(const std::string& url) const override { + GError* error = nullptr; + const gboolean ok = g_app_info_launch_default_for_uri(url.c_str(), nullptr, &error); + if (!ok) { + std::string message = "Failed to launch URL via desktop defaults."; + if (error && error->message) { + message = error->message; + } + if (error) { + g_error_free(error); + } + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = message; + return result; + } + + UrlOpenResult result; + result.success = true; + result.error_code = UrlOpenErrorCode::kNone; + return result; + } +}; + +} // namespace + +UrlOpener::UrlOpener() : pimpl_(std::make_unique()) {} + +UrlOpener::~UrlOpener() = default; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/window_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/window_linux.cpp new file mode 100644 index 0000000..1e3eecb --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/window_linux.cpp @@ -0,0 +1,647 @@ +#include +#include +#include +#include "../../foundation/id_allocator.h" +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" + +// Import GTK headers +#include +#include + +namespace nativeapi { + +// Key to store/retrieve WindowId on GObjects +static const char* kWindowIdKey = "NativeAPIWindowId"; + +// Helper function to find header bar in widget hierarchy +static GtkWidget* FindHeaderBar(GtkWidget* widget) { + if (!widget) + return nullptr; + + // Check if this widget is a header bar + if (GTK_IS_HEADER_BAR(widget)) + return widget; + + // If it's a container, search children + if (GTK_IS_CONTAINER(widget)) { + GList* children = gtk_container_get_children(GTK_CONTAINER(widget)); + for (GList* l = children; l != nullptr; l = l->next) { + GtkWidget* child = GTK_WIDGET(l->data); + GtkWidget* result = FindHeaderBar(child); + if (result) { + g_list_free(children); + return result; + } + } + g_list_free(children); + } + + return nullptr; +} + +// Private implementation class +class Window::Impl { + public: + Impl(GtkWidget* widget, GdkWindow* gdk_window) + : widget_(widget), + gdk_window_(gdk_window), + title_bar_style_(TitleBarStyle::Normal), + visual_effect_(VisualEffect::None), + background_color_(Color::White) {} + GtkWidget* widget_; + GdkWindow* gdk_window_; + TitleBarStyle title_bar_style_; + VisualEffect visual_effect_; + Color background_color_; +}; + +Window::Window() { + // Check if GTK is available + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + std::cerr << "No display available for window creation" << std::endl; + pimpl_ = std::make_unique(nullptr, nullptr); + return; + } + + // Create a new GTK toplevel window + GtkWidget* widget = gtk_window_new(GTK_WINDOW_TOPLEVEL); + if (!widget) { + std::cerr << "Failed to create GTK window" << std::endl; + pimpl_ = std::make_unique(nullptr, nullptr); + return; + } + + // Realize to ensure GdkWindow exists + if (!gtk_widget_get_realized(widget)) { + gtk_widget_realize(widget); + } + + // Obtain GdkWindow + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (!gdk_window) { + std::cerr << "Failed to get GdkWindow from GTK widget" << std::endl; + gtk_widget_destroy(widget); + pimpl_ = std::make_unique(nullptr, nullptr); + return; + } + + // Allocate and attach a stable WindowId to the native objects + WindowId id = IdAllocator::Allocate(); + if (id != IdAllocator::kInvalidId) { + g_object_set_data(G_OBJECT(widget), kWindowIdKey, + reinterpret_cast(static_cast(id))); + g_object_set_data(G_OBJECT(gdk_window), kWindowIdKey, + reinterpret_cast(static_cast(id))); + } + + // Only create the instance, don't show the window + pimpl_ = std::make_unique(widget, gdk_window); +} + +Window::Window(void* native_window) { + // Wrap existing GdkWindow or GtkWidget + GtkWidget* widget = nullptr; + GdkWindow* gdk_window = nullptr; + + // Heuristic: if this looks like a GtkWidget*, use it; otherwise treat as GdkWindow* + // In our codebase, native Linux window handles should be GtkWidget* (GtkWindow) + widget = static_cast(native_window); + if (widget && GTK_IS_WIDGET(widget)) { + if (!gtk_widget_get_realized(widget)) { + gtk_widget_realize(widget); + } + gdk_window = gtk_widget_get_window(widget); + } else { + // Fallback: assume GdkWindow* + gdk_window = static_cast(native_window); + } + + pimpl_ = std::make_unique(widget, gdk_window); +} + +Window::~Window() {} + +WindowId Window::GetId() const { + // Prefer reading ID stored on the native objects + if (pimpl_->gdk_window_) { + gpointer data = g_object_get_data(G_OBJECT(pimpl_->gdk_window_), kWindowIdKey); + if (data) { + return static_cast(reinterpret_cast(data)); + } + } + if (pimpl_->widget_) { + gpointer data = g_object_get_data(G_OBJECT(pimpl_->widget_), kWindowIdKey); + if (data) { + return static_cast(reinterpret_cast(data)); + } + } + return IdAllocator::kInvalidId; +} + +void Window::Focus() { + if (pimpl_->widget_) { + gtk_window_present(GTK_WINDOW(pimpl_->widget_)); + } else if (pimpl_->gdk_window_) { + gdk_window_focus(pimpl_->gdk_window_, GDK_CURRENT_TIME); + } +} + +void Window::Blur() { + if (pimpl_->gdk_window_) { + gdk_window_lower(pimpl_->gdk_window_); + } +} + +bool Window::IsFocused() const { + if (!pimpl_->gdk_window_) + return false; + // Check if this window is the focus window of its display + GdkDisplay* display = gdk_window_get_display(pimpl_->gdk_window_); + GdkSeat* seat = gdk_display_get_default_seat(display); + if (seat) { + GdkDevice* keyboard = gdk_seat_get_keyboard(seat); + if (keyboard) { + GdkWindow* focus_window = gdk_device_get_window_at_position(keyboard, nullptr, nullptr); + return focus_window == pimpl_->gdk_window_; + } + } + return false; +} + +void Window::Show() { + if (pimpl_->widget_) { + gtk_widget_show(pimpl_->widget_); + } else if (pimpl_->gdk_window_) { + gdk_window_show(pimpl_->gdk_window_); + } +} + +void Window::ShowInactive() { + if (pimpl_->widget_) { + gtk_widget_show(pimpl_->widget_); + } else if (pimpl_->gdk_window_) { + gdk_window_show_unraised(pimpl_->gdk_window_); + } +} + +void Window::Hide() { + if (pimpl_->widget_) { + gtk_widget_hide(pimpl_->widget_); + } else if (pimpl_->gdk_window_) { + gdk_window_hide(pimpl_->gdk_window_); + } +} + +bool Window::IsVisible() const { + if (pimpl_->widget_) { + return gtk_widget_get_visible(pimpl_->widget_); + } + if (pimpl_->gdk_window_) { + return gdk_window_is_visible(pimpl_->gdk_window_); + } + return false; +} + +void Window::Maximize() { + if (pimpl_->gdk_window_) { + gdk_window_maximize(pimpl_->gdk_window_); + } +} + +void Window::Unmaximize() { + if (pimpl_->gdk_window_) { + gdk_window_unmaximize(pimpl_->gdk_window_); + } +} + +bool Window::IsMaximized() const { + if (!pimpl_->gdk_window_) + return false; + GdkWindowState state = gdk_window_get_state(pimpl_->gdk_window_); + return state & GDK_WINDOW_STATE_MAXIMIZED; +} + +void Window::Minimize() { + if (pimpl_->gdk_window_) { + gdk_window_iconify(pimpl_->gdk_window_); + } +} + +void Window::Restore() { + if (pimpl_->gdk_window_) { + gdk_window_deiconify(pimpl_->gdk_window_); + } +} + +bool Window::IsMinimized() const { + if (!pimpl_->gdk_window_) + return false; + GdkWindowState state = gdk_window_get_state(pimpl_->gdk_window_); + return state & GDK_WINDOW_STATE_ICONIFIED; +} + +void Window::SetFullScreen(bool is_full_screen) { + if (!pimpl_->gdk_window_) + return; + if (is_full_screen) { + gdk_window_fullscreen(pimpl_->gdk_window_); + } else { + gdk_window_unfullscreen(pimpl_->gdk_window_); + } +} + +bool Window::IsFullScreen() const { + if (!pimpl_->gdk_window_) + return false; + GdkWindowState state = gdk_window_get_state(pimpl_->gdk_window_); + return state & GDK_WINDOW_STATE_FULLSCREEN; +} + +void Window::SetBounds(Rectangle bounds) { + if (pimpl_->gdk_window_) { + gdk_window_move_resize(pimpl_->gdk_window_, (gint)bounds.x, (gint)bounds.y, (gint)bounds.width, + (gint)bounds.height); + } +} + +Rectangle Window::GetBounds() const { + Rectangle bounds = {0, 0, 0, 0}; + if (pimpl_->gdk_window_) { + gint x, y, width, height; + gdk_window_get_geometry(pimpl_->gdk_window_, &x, &y, &width, &height); + bounds.x = x; + bounds.y = y; + bounds.width = width; + bounds.height = height; + } + return bounds; +} + +void Window::SetSize(Size size, bool animate) { + if (pimpl_->gdk_window_) { + gdk_window_resize(pimpl_->gdk_window_, (gint)size.width, (gint)size.height); + } +} + +Size Window::GetSize() const { + Size size = {0, 0}; + if (pimpl_->gdk_window_) { + gint width, height; + gdk_window_get_geometry(pimpl_->gdk_window_, nullptr, nullptr, &width, &height); + size.width = width; + size.height = height; + } + return size; +} + +void Window::SetContentSize(Size size) { + // For GDK windows, content size is the same as window size + SetSize(size, false); +} + +Size Window::GetContentSize() const { + // For GDK windows, content size is the same as window size + return GetSize(); +} + +void Window::SetContentBounds(Rectangle bounds) { + // For GDK windows, content bounds is the same as window bounds + SetBounds(bounds); +} + +Rectangle Window::GetContentBounds() const { + // For GDK windows, content bounds is the same as window bounds + return GetBounds(); +} + +void Window::SetMinimumSize(Size size) { + // GTK minimum size constraints would need to be set on the widget level + // For now, we'll provide a basic implementation that doesn't enforce + // constraints +} + +Size Window::GetMinimumSize() const { + return Size{0, 0}; +} + +void Window::SetMaximumSize(Size size) { + // GTK maximum size constraints would need to be set on the widget level + // For now, we'll provide a basic implementation that doesn't enforce + // constraints +} + +Size Window::GetMaximumSize() const { + return Size{-1, -1}; // -1 indicates no maximum +} + +void Window::SetResizable(bool is_resizable) { + // This would typically be set at window creation time in GTK + // For now, provide stub implementation +} + +bool Window::IsResizable() const { + return true; // Default assumption +} + +void Window::SetMovable(bool is_movable) { + // Window movability is typically a window manager property + // Provide stub implementation +} + +bool Window::IsMovable() const { + return true; // Default assumption +} + +void Window::SetMinimizable(bool is_minimizable) { + // This would typically be set via window hints + // Provide stub implementation +} + +bool Window::IsMinimizable() const { + return true; // Default assumption +} + +void Window::SetMaximizable(bool is_maximizable) { + // This would typically be set via window hints + // Provide stub implementation +} + +bool Window::IsMaximizable() const { + return true; // Default assumption +} + +void Window::SetFullScreenable(bool is_full_screenable) { + // Provide stub implementation +} + +bool Window::IsFullScreenable() const { + return true; // Default assumption +} + +void Window::SetClosable(bool is_closable) { + // This would typically be set via window hints + // Provide stub implementation +} + +bool Window::IsClosable() const { + return true; // Default assumption +} + +void Window::SetWindowControlButtonsVisible(bool is_visible) { + // TODO: Implement for Linux + // This would involve manipulating GTK window decorations +} + +bool Window::IsWindowControlButtonsVisible() const { + // TODO: Implement for Linux + return true; // Default to visible +} + +void Window::SetAlwaysOnTop(bool is_always_on_top) { + if (pimpl_->gdk_window_) { + gdk_window_set_keep_above(pimpl_->gdk_window_, is_always_on_top); + } +} + +bool Window::IsAlwaysOnTop() const { + if (!pimpl_->gdk_window_) + return false; + GdkWindowState state = gdk_window_get_state(pimpl_->gdk_window_); + return state & GDK_WINDOW_STATE_ABOVE; +} + +void Window::SetPosition(Point point) { + if (pimpl_->gdk_window_) { + gdk_window_move(pimpl_->gdk_window_, (gint)point.x, (gint)point.y); + } +} + +Point Window::GetPosition() const { + Point point = {0, 0}; + if (pimpl_->gdk_window_) { + gint x, y; + gdk_window_get_position(pimpl_->gdk_window_, &x, &y); + point.x = x; + point.y = y; + } + return point; +} + +void Window::Center() { + if (!pimpl_->gdk_window_) + return; + + // Get the window size + gint window_width, window_height; + gdk_window_get_geometry(pimpl_->gdk_window_, nullptr, nullptr, &window_width, &window_height); + + // Get the screen size + GdkDisplay* display = gdk_window_get_display(pimpl_->gdk_window_); + GdkMonitor* monitor = gdk_display_get_primary_monitor(display); + if (!monitor) { + // Fallback to first monitor if no primary monitor is found + monitor = gdk_display_get_monitor(display, 0); + } + + if (monitor) { + GdkRectangle geometry; + gdk_monitor_get_geometry(monitor, &geometry); + + // Calculate center position + gint center_x = geometry.x + (geometry.width - window_width) / 2; + gint center_y = geometry.y + (geometry.height - window_height) / 2; + + // Move the window to center + gdk_window_move(pimpl_->gdk_window_, center_x, center_y); + } +} + +void Window::SetTitle(std::string title) { + // Prefer setting title via GtkWindow if available + if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) { + gtk_window_set_title(GTK_WINDOW(pimpl_->widget_), title.c_str()); + return; + } + + // If only GdkWindow is available, try to get associated GtkWindow + if (pimpl_->gdk_window_) { + gpointer user_data = nullptr; + gdk_window_get_user_data(pimpl_->gdk_window_, &user_data); + if (user_data && GTK_IS_WINDOW(user_data)) { + gtk_window_set_title(GTK_WINDOW(user_data), title.c_str()); + return; + } + + // Fallback: set title via GDK for toplevel windows + gdk_window_set_title(pimpl_->gdk_window_, title.c_str()); + } +} + +std::string Window::GetTitle() const { + // Prefer reading title via GtkWindow if available + if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) { + const gchar* t = gtk_window_get_title(GTK_WINDOW(pimpl_->widget_)); + return t ? std::string(t) : std::string(); + } + + // If only GdkWindow is available, try to get associated GtkWindow + if (pimpl_->gdk_window_) { + gpointer user_data = nullptr; + gdk_window_get_user_data(pimpl_->gdk_window_, &user_data); + if (user_data && GTK_IS_WINDOW(user_data)) { + const gchar* t = gtk_window_get_title(GTK_WINDOW(user_data)); + return t ? std::string(t) : std::string(); + } + } + + // No reliable way to get title directly from GdkWindow + return std::string(); +} + +void Window::SetTitleBarStyle(TitleBarStyle style) { + pimpl_->title_bar_style_ = style; + + if (!pimpl_->widget_ || !GTK_IS_WINDOW(pimpl_->widget_)) + return; + + GtkWindow* gtk_window = GTK_WINDOW(pimpl_->widget_); + bool show_decorations = (style == TitleBarStyle::Normal); + + // Try to find and toggle header bar visibility + GtkWidget* header_bar = FindHeaderBar(pimpl_->widget_); + if (header_bar) { + gtk_widget_set_visible(header_bar, show_decorations); + } else { + // If no header bar found, toggle window decorations + const gchar* title = gtk_window_get_title(gtk_window); + if (title != nullptr) { + gtk_window_set_decorated(gtk_window, show_decorations); + } + } + + // When restoring to normal, ensure decorations are shown + if (show_decorations) { + gtk_window_set_decorated(gtk_window, TRUE); + } +} + +TitleBarStyle Window::GetTitleBarStyle() const { + return pimpl_->title_bar_style_; +} + +void Window::SetHasShadow(bool has_shadow) { + // Window shadows are typically managed by the window manager + // Provide stub implementation +} + +bool Window::HasShadow() const { + return true; // Default assumption +} + +void Window::SetOpacity(float opacity) { + if (pimpl_->gdk_window_) { + gdk_window_set_opacity(pimpl_->gdk_window_, opacity); + } +} + +float Window::GetOpacity() const { + // GDK doesn't provide a direct way to get opacity + return 1.0f; // Default assumption +} + +void Window::SetVisualEffect(VisualEffect effect) { + pimpl_->visual_effect_ = effect; + // TODO: Implement background blur for Linux (GTK/GDK) + // This typically requires compositor support or specific GTK CSS +} + +VisualEffect Window::GetVisualEffect() const { + return pimpl_->visual_effect_; +} + +void Window::SetBackgroundColor(const Color& color) { + if (!pimpl_->widget_) + return; + + // Store the color + pimpl_->background_color_ = color; + + // Create CSS provider for background color + GtkCssProvider* provider = gtk_css_provider_new(); + + // Format CSS string with RGBA color + gchar* css = g_strdup_printf( + "window { background-color: rgba(%d, %d, %d, %.2f); }", + color.r, color.g, color.b, color.a / 255.0); + + gtk_css_provider_load_from_data(provider, css, -1, nullptr); + g_free(css); + + // Apply CSS to the widget + GtkStyleContext* context = gtk_widget_get_style_context(pimpl_->widget_); + gtk_style_context_add_provider(context, + GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_object_unref(provider); +} + +Color Window::GetBackgroundColor() const { + if (!pimpl_->widget_) + return Color::White; + + // Return the stored background color + // Since we set it via CSS, we track it ourselves to avoid using deprecated APIs + return pimpl_->background_color_; +} + +void Window::SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces) { + if (pimpl_->gdk_window_) { + gdk_window_stick(pimpl_->gdk_window_); + } +} + +bool Window::IsVisibleOnAllWorkspaces() const { + if (!pimpl_->gdk_window_) + return false; + GdkWindowState state = gdk_window_get_state(pimpl_->gdk_window_); + return state & GDK_WINDOW_STATE_STICKY; +} + +void Window::SetIgnoreMouseEvents(bool is_ignore_mouse_events) { + // This would involve setting input shapes or event masks + // Provide stub implementation +} + +bool Window::IsIgnoreMouseEvents() const { + return false; // Default assumption +} + +void Window::SetFocusable(bool is_focusable) { + // This would typically be set via window hints + // Provide stub implementation +} + +bool Window::IsFocusable() const { + return true; // Default assumption +} + +void Window::StartDragging() { + // Window dragging would typically involve listening to mouse events + // Provide stub implementation +} + +void Window::StartResizing() { + // Window resizing would typically involve listening to mouse events at edges + // Provide stub implementation +} + +void* Window::GetNativeObjectInternal() const { + // Return the GtkWidget* (GtkWindow) as the native handle on Linux + return pimpl_ ? static_cast(pimpl_->widget_ ? pimpl_->widget_ : nullptr) : nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/linux/window_manager_linux.cpp b/packages/cnativeapi/cxx_impl/src/platform/linux/window_manager_linux.cpp new file mode 100644 index 0000000..f8903d6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/linux/window_manager_linux.cpp @@ -0,0 +1,490 @@ +#include +#include +#include +#include +#include +#include + +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" + +// Import GTK headers +#include +#include + +namespace nativeapi { + +// Key to store/retrieve WindowId on GObjects (must match window_linux.cpp) +static const char* kWindowIdKey = "NativeAPIWindowId"; + +// Shared static variables for window ID mapping +static std::unordered_map g_window_id_map; +static std::mutex g_map_mutex; + +// Track widgets that have been hooked to avoid duplicate connections +static std::set g_hooked_widgets; +static std::mutex g_hook_mutex; + +// Flag to indicate if global swizzling has been installed +static bool g_swizzle_installed = false; + +// Helper function to manage mapping between GdkWindow pointers and WindowIds +static WindowId GetOrCreateWindowId(GdkWindow* gdk_window) { + if (!gdk_window) { + return IdAllocator::kInvalidId; + } + + // First, try to read ID attached to the GObject + gpointer data = g_object_get_data(G_OBJECT(gdk_window), kWindowIdKey); + if (data) { + WindowId id = static_cast(reinterpret_cast(data)); + // Cache it in the map for faster lookup next time + std::lock_guard lock(g_map_mutex); + g_window_id_map[gdk_window] = id; + return id; + } + + // Fallback to cached map + { + std::lock_guard lock(g_map_mutex); + auto it = g_window_id_map.find(gdk_window); + if (it != g_window_id_map.end()) { + return it->second; + } + } + + // Allocate new ID and attach to the GObject for consistency + WindowId new_id = IdAllocator::Allocate(); + if (new_id != IdAllocator::kInvalidId) { + g_object_set_data(G_OBJECT(gdk_window), kWindowIdKey, + reinterpret_cast(static_cast(new_id))); + std::lock_guard lock(g_map_mutex); + g_window_id_map[gdk_window] = new_id; + } + return new_id; +} + +// Helper function to find GdkWindow by WindowId +static GdkWindow* FindGdkWindowById(WindowId id) { + std::lock_guard lock(g_map_mutex); + for (const auto& pair : g_window_id_map) { + if (pair.second == id) { + return pair.first; + } + } + return nullptr; +} + +// Forward declarations for swizzling functions +static void InstallShowHideHooks(GtkWidget* widget); +static void InstallGlobalSwizzling(); + +// Signal emission hook for show signal +static gboolean on_show_emission_hook(GSignalInvocationHint* ihint, + guint n_param_values, + const GValue* param_values, + gpointer data) { + (void)ihint; + (void)n_param_values; + (void)data; + + GtkWidget* widget = GTK_WIDGET(g_value_get_object(¶m_values[0])); + if (widget && GTK_IS_WINDOW(widget)) { + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + WindowId id = GetOrCreateWindowId(gdk_window); + WindowManager::GetInstance().HandleWillShow(id); + } + } + + return TRUE; // Continue emission +} + +// Signal emission hook for hide signal +static gboolean on_hide_emission_hook(GSignalInvocationHint* ihint, + guint n_param_values, + const GValue* param_values, + gpointer data) { + (void)ihint; + (void)n_param_values; + (void)data; + + GtkWidget* widget = GTK_WIDGET(g_value_get_object(¶m_values[0])); + if (widget && GTK_IS_WINDOW(widget)) { + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + WindowId id = GetOrCreateWindowId(gdk_window); + WindowManager::GetInstance().HandleWillHide(id); + } + } + + return TRUE; // Continue emission +} + +// Signal emission hook for delete-event signal +static gboolean on_delete_event_emission_hook(GSignalInvocationHint* ihint, + guint n_param_values, + const GValue* param_values, + gpointer data) { + (void)ihint; + (void)n_param_values; + (void)data; + + GtkWidget* widget = GTK_WIDGET(g_value_get_object(¶m_values[0])); + if (widget && GTK_IS_WINDOW(widget)) { + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + WindowId id = GetOrCreateWindowId(gdk_window); + WindowManager::GetInstance().HandleWillClose(id); + } + } + + return TRUE; // Continue emission +} + +// GTK signal callbacks to invoke hooks (used as fallback) +static gboolean OnGtkMapEvent(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + (void)event; + (void)user_data; + + if (GTK_IS_WINDOW(widget)) { + auto& manager = WindowManager::GetInstance(); + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + WindowId id = GetOrCreateWindowId(gdk_window); + manager.HandleWillShow(id); + } + } + // Return FALSE to propagate event further + return FALSE; +} + +static gboolean OnGtkUnmapEvent(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + (void)event; + (void)user_data; + + if (GTK_IS_WINDOW(widget)) { + auto& manager = WindowManager::GetInstance(); + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + WindowId id = GetOrCreateWindowId(gdk_window); + manager.HandleWillHide(id); + } + } + // Return FALSE to propagate event further + return FALSE; +} + +// Install hooks for a specific widget +static void InstallShowHideHooks(GtkWidget* widget) { + if (!widget || !GTK_IS_WINDOW(widget)) { + return; + } + + std::lock_guard lock(g_hook_mutex); + + // Check if already hooked + if (g_hooked_widgets.find(widget) != g_hooked_widgets.end()) { + return; + } + + // Connect map/unmap events as fallback + g_signal_connect(G_OBJECT(widget), "map-event", G_CALLBACK(OnGtkMapEvent), nullptr); + g_signal_connect(G_OBJECT(widget), "unmap-event", G_CALLBACK(OnGtkUnmapEvent), nullptr); + + g_hooked_widgets.insert(widget); +} + +// Install global swizzling using signal emission hooks +static void InstallGlobalSwizzling() { + if (g_swizzle_installed) { + return; + } + + // Get the show, hide, and delete-event signal IDs for GtkWidget + guint show_signal_id = g_signal_lookup("show", GTK_TYPE_WIDGET); + guint hide_signal_id = g_signal_lookup("hide", GTK_TYPE_WIDGET); + guint delete_event_signal_id = g_signal_lookup("delete-event", GTK_TYPE_WIDGET); + + if (show_signal_id != 0) { + // Add emission hook for show signal + g_signal_add_emission_hook(show_signal_id, 0, on_show_emission_hook, nullptr, nullptr); + } + + if (hide_signal_id != 0) { + // Add emission hook for hide signal + g_signal_add_emission_hook(hide_signal_id, 0, on_hide_emission_hook, nullptr, nullptr); + } + + if (delete_event_signal_id != 0) { + // Add emission hook for delete-event signal + g_signal_add_emission_hook(delete_event_signal_id, 0, on_delete_event_emission_hook, nullptr, nullptr); + } + + g_swizzle_installed = true; +} + +// Private implementation for Linux +class WindowManager::Impl { + public: + Impl(WindowManager* manager) : manager_(manager) {} + ~Impl() {} + + void StartEventListening() { + // Install global swizzling for show/hide interception + InstallGlobalSwizzling(); + + // Monitor all existing windows + GdkDisplay* display = gdk_display_get_default(); + if (display) { + GList* toplevels = gtk_window_list_toplevels(); + for (GList* l = toplevels; l != nullptr; l = l->next) { + GtkWindow* gtk_window = GTK_WINDOW(l->data); + InstallShowHideHooks(GTK_WIDGET(gtk_window)); + } + g_list_free(toplevels); + } + } + + void StopEventListening() { + // Clear hooked widgets set + std::lock_guard lock(g_hook_mutex); + g_hooked_widgets.clear(); + } + + private: + WindowManager* manager_; + // Optional pre-show/hide/close hooks + std::optional will_show_hook_; + std::optional will_hide_hook_; + std::optional will_close_hook_; + + friend class WindowManager; +}; + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + // Try to initialize GTK if not already initialized + // In headless environments, this may fail, which is acceptable + if (!gdk_display_get_default()) { + // Temporarily redirect stderr to suppress GTK warnings in headless + // environments + FILE* original_stderr = stderr; + freopen("/dev/null", "w", stderr); + + gboolean gtk_result = gtk_init_check(nullptr, nullptr); + + // Restore stderr + fflush(stderr); + freopen("/dev/tty", "w", stderr); + stderr = original_stderr; + + // gtk_init_check returns FALSE if initialization failed (e.g., no display) + // This is acceptable for headless environments + } + + StartEventListening(); +} + +WindowManager::~WindowManager() { + StopEventListening(); +} + +std::shared_ptr WindowManager::Get(WindowId id) { + auto cached = WindowRegistry::GetInstance().Get(id); + if (cached) { + return cached; + } + + // Try to find the window by ID in the current display + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + return nullptr; + } + + // Get all toplevel windows + GList* toplevels = gtk_window_list_toplevels(); + for (GList* l = toplevels; l != nullptr; l = l->next) { + GtkWindow* gtk_window = GTK_WINDOW(l->data); + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(gtk_window)); + + if (gdk_window && GetOrCreateWindowId(gdk_window) == id) { + auto window = std::make_shared((void*)gdk_window); + WindowRegistry::GetInstance().Add(id, window); + g_list_free(toplevels); + return window; + } + } + g_list_free(toplevels); + return nullptr; +} + +std::vector> WindowManager::GetAll() { + std::vector> windows; + + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + return windows; + } + + // Get all toplevel windows + GList* toplevels = gtk_window_list_toplevels(); + for (GList* l = toplevels; l != nullptr; l = l->next) { + GtkWindow* gtk_window = GTK_WINDOW(l->data); + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(gtk_window)); + + if (gdk_window) { + WindowId window_id = GetOrCreateWindowId(gdk_window); + if (!WindowRegistry::GetInstance().Get(window_id)) { + auto window = std::make_shared((void*)gdk_window); + WindowRegistry::GetInstance().Add(window_id, window); + } + } + } + g_list_free(toplevels); + + // Return all cached windows + return WindowRegistry::GetInstance().GetAll(); +} + +std::shared_ptr WindowManager::GetCurrent() { + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + return nullptr; + } + + // Try to get the focused window + GdkSeat* seat = gdk_display_get_default_seat(display); + if (seat) { + GdkDevice* keyboard = gdk_seat_get_keyboard(seat); + if (keyboard) { + GdkWindow* focused_window = gdk_device_get_window_at_position(keyboard, nullptr, nullptr); + if (focused_window) { + WindowId window_id = GetOrCreateWindowId(focused_window); + return Get(window_id); + } + } + } + + // Fallback: get the first visible window + GList* toplevels = gtk_window_list_toplevels(); + for (GList* l = toplevels; l != nullptr; l = l->next) { + GtkWindow* gtk_window = GTK_WINDOW(l->data); + if (gtk_widget_get_visible(GTK_WIDGET(gtk_window))) { + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(gtk_window)); + if (gdk_window) { + WindowId window_id = GetOrCreateWindowId(gdk_window); + g_list_free(toplevels); + return Get(window_id); + } + } + } + g_list_free(toplevels); + + return nullptr; +} + +void WindowManager::SetWillShowHook(std::optional hook) { + pimpl_->will_show_hook_ = std::move(hook); + if (pimpl_->will_show_hook_) { + // Ensure global swizzling is installed when hook is set + InstallGlobalSwizzling(); + } +} + +void WindowManager::SetWillHideHook(std::optional hook) { + pimpl_->will_hide_hook_ = std::move(hook); + if (pimpl_->will_hide_hook_) { + // Ensure global swizzling is installed when hook is set + InstallGlobalSwizzling(); + } +} + +void WindowManager::SetWillCloseHook(std::optional hook) { + pimpl_->will_close_hook_ = std::move(hook); + if (pimpl_->will_close_hook_) { + // Ensure global swizzling is installed when hook is set + InstallGlobalSwizzling(); + } +} + +bool WindowManager::HasWillShowHook() const { + return pimpl_->will_show_hook_.has_value(); +} + +bool WindowManager::HasWillHideHook() const { + return pimpl_->will_hide_hook_.has_value(); +} + +bool WindowManager::HasWillCloseHook() const { + return pimpl_->will_close_hook_.has_value(); +} + +void WindowManager::HandleWillShow(WindowId id) { + if (pimpl_->will_show_hook_) { + (*pimpl_->will_show_hook_)(id); + } +} + +void WindowManager::HandleWillHide(WindowId id) { + if (pimpl_->will_hide_hook_) { + (*pimpl_->will_hide_hook_)(id); + } +} + +void WindowManager::HandleWillClose(WindowId id) { + if (pimpl_->will_close_hook_) { + (*pimpl_->will_close_hook_)(id); + } +} + +bool WindowManager::CallOriginalShow(WindowId id) { + GdkWindow* gdk_window = FindGdkWindowById(id); + if (!gdk_window) { + return false; + } + + // Call the original GDK show function directly + gdk_window_show(gdk_window); + return true; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + GdkWindow* gdk_window = FindGdkWindowById(id); + if (!gdk_window) { + return false; + } + + // Call the original GDK hide function directly + gdk_window_hide(gdk_window); + return true; +} + +bool WindowManager::CallOriginalClose(WindowId id) { + GdkWindow* gdk_window = FindGdkWindowById(id); + if (!gdk_window) { + return false; + } + + // On Linux, destroy the underlying GtkWidget to close the window + GtkWidget* widget = gtk_widget_get_toplevel(GTK_WIDGET(gdk_window)); + if (widget && GTK_IS_WINDOW(widget)) { + gtk_widget_destroy(widget); + return true; + } + return false; +} + +void WindowManager::StartEventListening() { + pimpl_->StartEventListening(); +} + +void WindowManager::StopEventListening() { + pimpl_->StopEventListening(); +} + +void WindowManager::DispatchWindowEvent(const WindowEvent& event) { + Emit(event); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/accessibility_manager_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/accessibility_manager_macos.mm new file mode 100644 index 0000000..70ca556 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/accessibility_manager_macos.mm @@ -0,0 +1,16 @@ +#import + +#include "../../accessibility_manager.h" + +namespace nativeapi { + +void AccessibilityManager::Enable() { + NSDictionary* options = @{(__bridge NSString*)kAXTrustedCheckOptionPrompt : @YES}; + AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); +} + +bool AccessibilityManager::IsEnabled() { + return AXIsProcessTrustedWithOptions(nil); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/application_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/application_macos.mm new file mode 100644 index 0000000..cbe3a42 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/application_macos.mm @@ -0,0 +1,261 @@ +#import +#import +#include +#include +#include +#include +#include + +#include "../../application.h" +#include "../../menu.h" +#include "../../window_manager.h" + +@interface NativeApplicationDelegate : NSObject +@property(nonatomic, assign) nativeapi::Application* app; +@end + +@implementation NativeApplicationDelegate + +- (void)applicationDidFinishLaunching:(NSNotification*)notification { + // Emit application started event + nativeapi::ApplicationStartedEvent event; + self.app->Emit(event); +} + +- (void)applicationWillTerminate:(NSNotification*)notification { + // Emit application exiting event + nativeapi::ApplicationExitingEvent event(0); + self.app->Emit(event); +} + +- (void)applicationDidBecomeActive:(NSNotification*)notification { + // Emit application activated event + nativeapi::ApplicationActivatedEvent event; + self.app->Emit(event); +} + +- (void)applicationDidResignActive:(NSNotification*)notification { + // Emit application deactivated event + nativeapi::ApplicationDeactivatedEvent event; + self.app->Emit(event); +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender { + // Emit quit requested event + nativeapi::ApplicationQuitRequestedEvent event; + self.app->Emit(event); + + // Allow termination + return NSTerminateNow; +} + +@end + +namespace nativeapi { + +class Application::Impl { + public: + Impl(Application* app) : app_(app), delegate_(nullptr) {} + ~Impl() = default; + + bool Initialize() { + // Ensure we're on the main thread + if (![NSThread isMainThread]) { + return false; + } + + // Get or create NSApplication instance + NSApplication* ns_app = [NSApplication sharedApplication]; + if (!ns_app) { + return false; + } + + // Set dock icon visible by default + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + // Create and set delegate + delegate_ = [[NativeApplicationDelegate alloc] init]; + delegate_.app = app_; + [ns_app setDelegate:delegate_]; + + return true; + } + + int Run() { + // Start the main event loop + [NSApp run]; + + return 0; + } + + int Run(std::shared_ptr window) { + if (!window) { + return -1; + } + + // Set the window as primary window + app_->SetPrimaryWindow(window); + + // Show the window + window->Show(); + window->Focus(); + + // Start the main event loop + [NSApp run]; + + return 0; + } + + void Quit(int exit_code) { [NSApp terminate:nil]; } + + bool SetIcon(const std::string& icon_path) { + if (icon_path.empty()) { + return false; + } + + NSString* path = [NSString stringWithUTF8String:icon_path.c_str()]; + NSImage* icon = [[NSImage alloc] initWithContentsOfFile:path]; + + if (!icon) { + return false; + } + + [NSApp setApplicationIconImage:icon]; + return true; + } + + bool SetDockIconVisible(bool visible) { + if (visible) { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + } else { + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + } + return true; + } + + bool SetMenuBar(std::shared_ptr menu) { + if (!menu) { + return false; + } + + // Get the native menu handle + NSMenu* ns_menu = (__bridge NSMenu*)(menu->GetNativeObject()); + if (!ns_menu) { + return false; + } + + // Set the application menu + [NSApp setMainMenu:ns_menu]; + + return true; + } + + void CleanupEventMonitoring() { + // Clean up macOS-specific event monitoring + if (lock_file_handle_ != -1) { + close(lock_file_handle_); + lock_file_handle_ = -1; + } + + if (delegate_) { + [NSApp setDelegate:nil]; + delegate_ = nil; + } + } + + private: + Application* app_; + NativeApplicationDelegate* delegate_; + int lock_file_handle_ = -1; +}; + +Application::Application() + : initialized_(true), running_(false), exit_code_(0), pimpl_(std::make_unique(this)) { + // Perform platform-specific initialization automatically + pimpl_->Initialize(); + + // Emit application started event + Emit(); +} + +Application::~Application() { + // Clean up platform-specific event monitoring + pimpl_->CleanupEventMonitoring(); +} + +int Application::Run() { + running_ = true; + + // Start the platform-specific main event loop + int result = pimpl_->Run(); + + running_ = false; + + // Emit exit event + Emit(result); + + return result; +} + +int Application::Run(std::shared_ptr window) { + if (!window) { + return -1; // Invalid window + } + + running_ = true; + + // Start the platform-specific main event loop with window + int result = pimpl_->Run(window); + + running_ = false; + + // Emit exit event + Emit(result); + + return result; +} + +void Application::Quit(int exit_code) { + exit_code_ = exit_code; + + // Emit quit requested event + Emit(); + + // Request platform-specific quit + pimpl_->Quit(exit_code); +} + +bool Application::IsRunning() const { + return running_; +} + +bool Application::IsSingleInstance() const { + return false; +} + +bool Application::SetIcon(const std::string& icon_path) { + return pimpl_->SetIcon(icon_path); +} + +bool Application::SetDockIconVisible(bool visible) { + return pimpl_->SetDockIconVisible(visible); +} + +bool Application::SetMenuBar(std::shared_ptr menu) { + return pimpl_->SetMenuBar(menu); +} + +std::shared_ptr Application::GetPrimaryWindow() const { + return primary_window_; +} + +void Application::SetPrimaryWindow(std::shared_ptr window) { + primary_window_ = window; +} + +std::vector> Application::GetAllWindows() const { + auto& window_manager = WindowManager::GetInstance(); + return window_manager.GetAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/coordinate_utils_macos.h b/packages/cnativeapi/cxx_impl/src/platform/macos/coordinate_utils_macos.h new file mode 100644 index 0000000..9afbf12 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/coordinate_utils_macos.h @@ -0,0 +1,51 @@ +#pragma once + +// Import Cocoa and Core Graphics headers +#import +#import + +namespace nativeapi { + +// NSRect extension-like helper for coordinate system conversion +struct NSRectExt { + // Convert NSRect with bottom-left origin to topLeft point + static CGPoint topLeft(NSRect rect) { + NSRect primaryScreenFrame = [[NSScreen screens][0] frame]; + return CGPointMake(rect.origin.x, + primaryScreenFrame.size.height - rect.origin.y - rect.size.height); + } + + // Convert NSRect with topLeft origin to NSRect with bottom-left origin + static NSRect bottomLeft(NSRect rect) { + NSRect primaryScreenFrame = [[NSScreen screens][0] frame]; + // Convert topLeft origin to bottom-left origin + double bottomY = primaryScreenFrame.size.height - rect.origin.y - rect.size.height; + return NSMakeRect(rect.origin.x, bottomY, rect.size.width, rect.size.height); + } +}; + +// NSPoint extension-like helper for coordinate system conversion +struct NSPointExt { + // Convert bottom-left point to top-left point + static CGPoint topLeft(NSPoint point) { + NSRect primaryScreenFrame = [[NSScreen screens][0] frame]; + return CGPointMake(point.x, primaryScreenFrame.size.height - point.y); + } + + // Convert top-left point to bottom-left point + // Note: For window positions, use bottomLeftForWindow instead, as it requires window height + static NSPoint bottomLeft(CGPoint topLeftPoint) { + NSRect primaryScreenFrame = [[NSScreen screens][0] frame]; + return NSMakePoint(topLeftPoint.x, primaryScreenFrame.size.height - topLeftPoint.y); + } + + // Convert top-left window position to bottom-left window position + // This is the correct method for converting window positions, as it accounts for window height + static NSPoint bottomLeftForWindow(CGPoint topLeftPoint, CGFloat windowHeight) { + NSRect primaryScreenFrame = [[NSScreen screens][0] frame]; + double bottomY = primaryScreenFrame.size.height - topLeftPoint.y - windowHeight; + return NSMakePoint(topLeftPoint.x, bottomY); + } +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/dispatcher_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/dispatcher_macos.mm new file mode 100644 index 0000000..678ad5f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/dispatcher_macos.mm @@ -0,0 +1,73 @@ +#include "../../foundation/dispatcher_platform.h" + +#import +#import +#import +#include + +namespace nativeapi { +namespace dispatcher_platform { + +bool PlatformIsMainThread() { + return [NSThread isMainThread]; +} + +void PlatformSetMainThread() { + // No-op: on Apple platforms the OS is authoritative about which thread is the + // main thread, so there is nothing for the caller to correct. +} + +bool PlatformIsMainThreadDispatchSupported() { + return true; +} + +bool PlatformRunOnMainThread(std::function fn) { + if (!fn) { + return true; + } + + // Heap-allocate rather than capturing the std::function in a __block variable: + // block capture of non-trivial C++ types differs between ARC and non-ARC + // translation units, and this file is compiled into both configurations. + auto* work = new std::function(std::move(fn)); + dispatch_async(dispatch_get_main_queue(), ^{ + (*work)(); + delete work; + }); + return true; +} + +bool PlatformRunMainThreadLoopFor(int timeout_ms) { + // Two queues have to be serviced here, and only one call does both. + // + // The GCD main queue carries RunOnMainThread() work, and draining it means + // running the main run loop. The Carbon event queue carries global hotkeys + // (see shortcut_manager_macos.mm) and other OS events; a bare + // CFRunLoopRunInMode() does *not* dispatch those, which is why a program + // without a Cocoa run loop would register a shortcut successfully and then + // never see it fire. + // + // ReceiveNextEvent() runs the main run loop internally — so it drains the + // GCD main queue too — and additionally hands back the next OS event, which + // we forward to the Carbon dispatcher. That is the same routing + // `[NSApp sendEvent:]` performs in a Cocoa app; an app that has one keeps + // using it and never calls this function. + // + // Must be called on the main thread: ReceiveNextEvent() drains the calling + // thread's event queue, and OS events are only ever posted to the main one. + if (!PlatformIsMainThread()) { + return false; + } + + EventRef event = nullptr; + const EventTimeout timeout = timeout_ms / 1000.0 * kEventDurationSecond; + OSStatus status = ReceiveNextEvent(0, nullptr, timeout, true, &event); + if (status == noErr && event) { + SendEventToEventTarget(event, GetEventDispatcherTarget()); + ReleaseEvent(event); + } + return true; +} + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/display_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/display_macos.mm new file mode 100644 index 0000000..7442086 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/display_macos.mm @@ -0,0 +1,160 @@ +#include "../../display.h" +#include "coordinate_utils_macos.h" + +// Import Cocoa and Core Graphics headers +#import +#import + +namespace nativeapi { + +static NSScreen* FindScreenByDisplayID(CGDirectDisplayID display_id) { + NSArray* screens = [NSScreen screens]; + for (NSScreen* screen in screens) { + CGDirectDisplayID screenDisplayID = + [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue]; + if (screenDisplayID == display_id) { + return screen; + } + } + return nil; +} + +// Private implementation class +class Display::Impl { + public: + Impl() = default; + + // Display instances are long-lived identity objects, but NSScreen objects + // are recreated on configuration changes. Resolve the screen by its + // CGDirectDisplayID on every access so getters always read live state, + // falling back to the wrapped screen for objects not in [NSScreen screens]. + NSScreen* Screen() const { + if (display_id_ != 0) { + NSScreen* screen = FindScreenByDisplayID(display_id_); + if (screen) { + return screen; + } + } + return ns_screen_; + } + + const DisplayId id_ = IdAllocator::Allocate(); + NSScreen* ns_screen_ = nil; + CGDirectDisplayID display_id_ = 0; +}; + +Display::Display(void* display) : pimpl_(std::make_unique()) { + if (display) { + // Assume the void* is either NSScreen* or CGDirectDisplayID* + // Try NSScreen first + NSScreen* screen = (__bridge NSScreen*)display; + if (screen && [screen isKindOfClass:[NSScreen class]]) { + pimpl_->ns_screen_ = screen; + pimpl_->display_id_ = + [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue]; + } else { + // Try CGDirectDisplayID + CGDirectDisplayID displayID = *(CGDirectDisplayID*)display; + pimpl_->display_id_ = displayID; + pimpl_->ns_screen_ = FindScreenByDisplayID(displayID); + } + } +} + +Display::~Display() = default; + +void* Display::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->Screen(); +} + +// Getters - directly read from NSScreen +DisplayId Display::GetId() const { + return pimpl_->id_; +} + +std::string Display::GetName() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return ""; + NSString* displayName; + if (@available(macOS 10.15, *)) { + displayName = [screen localizedName]; + } else { + displayName = [NSString stringWithFormat:@"Display %@", @(pimpl_->display_id_)]; + } + return [displayName UTF8String]; +} + +Point Display::GetPosition() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return {0.0, 0.0}; + NSRect frame = [screen frame]; + + // Convert from bottom-left (macOS default) to top-left coordinate system + CGPoint topLeft = NSRectExt::topLeft(frame); + + return {topLeft.x, topLeft.y}; +} + +Size Display::GetSize() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return {0.0, 0.0}; + NSRect frame = [screen frame]; + return {frame.size.width, frame.size.height}; +} + +Rectangle Display::GetWorkArea() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return {0.0, 0.0, 0.0, 0.0}; + NSRect visibleFrame = [screen visibleFrame]; + + // Convert from bottom-left (macOS default) to top-left coordinate system + CGPoint topLeft = NSRectExt::topLeft(visibleFrame); + + return {topLeft.x, topLeft.y, visibleFrame.size.width, visibleFrame.size.height}; +} + +double Display::GetScaleFactor() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return 1.0; + return [screen backingScaleFactor]; +} + +bool Display::IsPrimary() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return false; + NSArray* screens = [NSScreen screens]; + return screens.count > 0 && screens[0] == screen; +} + +DisplayOrientation Display::GetOrientation() const { + NSScreen* screen = pimpl_->Screen(); + if (!screen) + return DisplayOrientation::kPortrait; + NSRect frame = [screen frame]; + return (frame.size.width > frame.size.height) ? DisplayOrientation::kLandscape + : DisplayOrientation::kPortrait; +} + +int Display::GetRefreshRate() const { + if (!pimpl_->Screen()) + return 60; + CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode(pimpl_->display_id_); + if (displayMode) { + double refreshRate = CGDisplayModeGetRefreshRate(displayMode); + CGDisplayModeRelease(displayMode); + return refreshRate > 0 ? (int)refreshRate : 60; + } + return 60; +} + +int Display::GetBitDepth() const { + return 32; // Default for modern displays +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/display_manager_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/display_manager_macos.mm new file mode 100644 index 0000000..967eef3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/display_manager_macos.mm @@ -0,0 +1,61 @@ +#include +#include + +#include "../../display.h" +#include "../../display_manager.h" +#include "coordinate_utils_macos.h" + +// Import Cocoa and Core Graphics headers +#import +#import + +namespace nativeapi { + +id displayObserver_; + +DisplayManager::DisplayManager() { + // Prime the instance cache so the first change notification diffs against + // the displays present at startup. + GetAll(); + // Set up display configuration change observer + displayObserver_ = [[NSNotificationCenter defaultCenter] + addObserverForName:NSApplicationDidChangeScreenParametersNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* notification) { + HandleDisplaysChanged(); + }]; +} + +DisplayManager::~DisplayManager() { + if (displayObserver_) { + [[NSNotificationCenter defaultCenter] removeObserver:displayObserver_]; + } +} + +std::vector DisplayManager::EnumerateNativeDisplays() { + std::vector natives; + NSArray* screens = [NSScreen screens]; + bool isPrimary = true; // Only the first NSScreen is the primary display + for (NSScreen* screen in screens) { + CGDirectDisplayID displayID = + [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue]; + natives.push_back({std::to_string(displayID), (__bridge void*)screen, isPrimary}); + isPrimary = false; + } + return natives; +} + +Point DisplayManager::GetCursorPosition() { + NSPoint mouseLocation = [NSEvent mouseLocation]; + + // Convert from bottom-left (macOS default) to top-left coordinate system + CGPoint topLeftPoint = NSPointExt::topLeft(mouseLocation); + + Point point; + point.x = topLeftPoint.x; + point.y = topLeftPoint.y; + return point; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/image_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/image_macos.mm new file mode 100644 index 0000000..1d6515b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/image_macos.mm @@ -0,0 +1,210 @@ +#import +#import +#include +#include +#include +#include +#include "../../foundation/geometry.h" +#include "../../image.h" + +namespace nativeapi { + +// macOS-specific implementation of Image class +class Image::Impl { + public: + NSImage* ns_image_; + std::string source_; + Size size_; + std::string format_; + + Impl() : ns_image_(nil), size_({0, 0}), format_("Unknown") {} + + ~Impl() {} + + Impl(const Impl& other) + : ns_image_(nil), source_(other.source_), size_(other.size_), format_(other.format_) { + if (other.ns_image_) { + ns_image_ = [other.ns_image_ copy]; + } + } + + Impl& operator=(const Impl& other) { + if (this != &other) { + ns_image_ = nil; + source_ = other.source_; + size_ = other.size_; + format_ = other.format_; + if (other.ns_image_) { + ns_image_ = [other.ns_image_ copy]; + } + } + return *this; + } +}; + +Image::Image() : pimpl_(std::make_unique()) {} + +Image::~Image() = default; + +Image::Image(const Image& other) : pimpl_(std::make_unique(*other.pimpl_)) {} + +Image::Image(Image&& other) noexcept : pimpl_(std::move(other.pimpl_)) {} + +std::shared_ptr Image::FromFile(const std::string& file_path) { + auto image = std::shared_ptr(new Image()); + + NSString* nsFilePath = [NSString stringWithUTF8String:file_path.c_str()]; + NSImage* nsImage = [[NSImage alloc] initWithContentsOfFile:nsFilePath]; + + if (nsImage) { + image->pimpl_->ns_image_ = nsImage; + image->pimpl_->source_ = file_path; + + // Get actual image size + NSSize nsSize = [nsImage size]; + image->pimpl_->size_ = {static_cast(nsSize.width), static_cast(nsSize.height)}; + + // Determine format from file extension + NSString* extension = [[nsFilePath pathExtension] lowercaseString]; + if ([extension isEqualToString:@"png"]) { + image->pimpl_->format_ = "PNG"; + } else if ([extension isEqualToString:@"jpg"] || [extension isEqualToString:@"jpeg"]) { + image->pimpl_->format_ = "JPEG"; + } else if ([extension isEqualToString:@"gif"]) { + image->pimpl_->format_ = "GIF"; + } else if ([extension isEqualToString:@"tiff"] || [extension isEqualToString:@"tif"]) { + image->pimpl_->format_ = "TIFF"; + } else if ([extension isEqualToString:@"bmp"]) { + image->pimpl_->format_ = "BMP"; + } else if ([extension isEqualToString:@"ico"]) { + image->pimpl_->format_ = "ICO"; + } else if ([extension isEqualToString:@"pdf"]) { + image->pimpl_->format_ = "PDF"; + } else { + image->pimpl_->format_ = "Unknown"; + } + } else { + return nullptr; + } + + return image; +} + +std::shared_ptr Image::FromBase64(const std::string& base64_data) { + auto image = std::shared_ptr(new Image()); + + // Remove data URI prefix if present + std::string cleanBase64 = base64_data; + size_t commaPos = base64_data.find(','); + if (commaPos != std::string::npos) { + cleanBase64 = base64_data.substr(commaPos + 1); + } + + // Decode base64 + NSString* base64String = [NSString stringWithUTF8String:cleanBase64.c_str()]; + NSData* imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; + + if (imageData) { + NSImage* nsImage = [[NSImage alloc] initWithData:imageData]; + if (nsImage) { + image->pimpl_->ns_image_ = nsImage; + image->pimpl_->source_ = base64_data; + + // Get actual image size + NSSize nsSize = [nsImage size]; + image->pimpl_->size_ = {static_cast(nsSize.width), + static_cast(nsSize.height)}; + + // Default assumption for base64 images + image->pimpl_->format_ = "PNG"; + } else { + return nullptr; + } + } else { + return nullptr; + } + + return image; +} + +Size Image::GetSize() const { + return pimpl_->size_; +} + +std::string Image::GetFormat() const { + return pimpl_->format_; +} + +std::string Image::ToBase64() const { + if (!pimpl_->ns_image_) { + return ""; + } + + // Convert NSImage to PNG data + NSBitmapImageRep* bitmapRep = + [[NSBitmapImageRep alloc] initWithData:[pimpl_->ns_image_ TIFFRepresentation]]; + if (!bitmapRep) { + return ""; + } + + NSData* pngData = [bitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + + if (!pngData) { + return ""; + } + + // Convert to base64 + NSString* base64String = [pngData base64EncodedStringWithOptions:0]; + std::string result = "data:image/png;base64," + std::string([base64String UTF8String]); + + return result; +} + +bool Image::SaveToFile(const std::string& file_path) const { + if (!pimpl_->ns_image_) { + return false; + } + + NSString* nsFilePath = [NSString stringWithUTF8String:file_path.c_str()]; + NSString* extension = [[nsFilePath pathExtension] lowercaseString]; + + NSBitmapImageFileType fileType; + NSDictionary* properties = @{}; + + if ([extension isEqualToString:@"png"]) { + fileType = NSBitmapImageFileTypePNG; + } else if ([extension isEqualToString:@"jpg"] || [extension isEqualToString:@"jpeg"]) { + fileType = NSBitmapImageFileTypeJPEG; + properties = @{NSImageCompressionFactor : @0.9}; + } else if ([extension isEqualToString:@"gif"]) { + fileType = NSBitmapImageFileTypeGIF; + } else if ([extension isEqualToString:@"tiff"] || [extension isEqualToString:@"tif"]) { + fileType = NSBitmapImageFileTypeTIFF; + } else if ([extension isEqualToString:@"bmp"]) { + fileType = NSBitmapImageFileTypeBMP; + } else { + // Default to PNG + fileType = NSBitmapImageFileTypePNG; + } + + NSBitmapImageRep* bitmapRep = + [[NSBitmapImageRep alloc] initWithData:[pimpl_->ns_image_ TIFFRepresentation]]; + if (!bitmapRep) { + return false; + } + + NSData* imageData = [bitmapRep representationUsingType:fileType properties:properties]; + + if (!imageData) { + return false; + } + + BOOL success = [imageData writeToFile:nsFilePath atomically:YES]; + return success == YES; +} + +void* Image::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ns_image_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/keyboard_monitor_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/keyboard_monitor_macos.mm new file mode 100644 index 0000000..84b0623 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/keyboard_monitor_macos.mm @@ -0,0 +1,133 @@ +#include +#include +#include + +#include "../../keyboard_monitor.h" + +// Import Cocoa headers +#import +#import + +namespace nativeapi { + +class KeyboardMonitor::Impl { + public: + Impl(KeyboardMonitor* monitor) : monitor_(monitor) {} + + CFMachPortRef eventTap = nullptr; + CFRunLoopSourceRef runLoopSource = nullptr; + KeyboardMonitor* monitor_; +}; + +KeyboardMonitor::KeyboardMonitor() : impl_(std::make_unique(this)) {} + +KeyboardMonitor::~KeyboardMonitor() { + Stop(); +} + +// Callback function for keyboard events +static CGEventRef keyboardEventCallback(CGEventTapProxy proxy, + CGEventType type, + CGEventRef event, + void* refcon) { + auto* event_emitter = static_cast*>(refcon); + if (!event_emitter) + return event; + + // Get the key code + CGKeyCode keyCode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + + if (type == kCGEventKeyDown) { + KeyPressedEvent key_event(keyCode); + event_emitter->Emit(key_event); + } else if (type == kCGEventKeyUp) { + KeyReleasedEvent key_event(keyCode); + event_emitter->Emit(key_event); + } else if (type == kCGEventFlagsChanged) { + CGEventFlags flags = CGEventGetFlags(event); + uint32_t modifier_keys = static_cast(ModifierKey::None); + if (flags & kCGEventFlagMaskShift) { + modifier_keys |= static_cast(ModifierKey::Shift); + } + if (flags & kCGEventFlagMaskControl) { + modifier_keys |= static_cast(ModifierKey::Ctrl); + } + if (flags & kCGEventFlagMaskAlternate) { + modifier_keys |= static_cast(ModifierKey::Alt); + } + if (flags & kCGEventFlagMaskCommand) { + modifier_keys |= static_cast(ModifierKey::Meta); + } + if (flags & kCGEventFlagMaskSecondaryFn) { + modifier_keys |= static_cast(ModifierKey::Fn); + } + if (flags & kCGEventFlagMaskAlphaShift) { + modifier_keys |= static_cast(ModifierKey::CapsLock); + } + if (flags & kCGEventFlagMaskNumericPad) { + modifier_keys |= static_cast(ModifierKey::NumLock); + } + ModifierKeysChangedEvent modifier_event(modifier_keys); + event_emitter->Emit(modifier_event); + } + return event; +} + +void KeyboardMonitor::Start() { + if (impl_->eventTap != nullptr) { + return; // Already started + } + + // Create event mask + CGEventMask eventMask = + (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) | (1 << kCGEventFlagsChanged); + + // Create event tap for keyboard events + impl_->eventTap = + CGEventTapCreate(kCGSessionEventTap, // Monitor session-wide events + kCGHeadInsertEventTap, // Insert at the head of the event queue + kCGEventTapOptionDefault, // Default options + eventMask, // Monitor key down, up, and flags changed events + keyboardEventCallback, + this); // Pass this pointer as user data + + if (impl_->eventTap == nullptr) { + std::cerr << "Failed to create event tap" << std::endl; + return; + } + + // Create a run loop source + impl_->runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, impl_->eventTap, 0); + + // Add to the current run loop + CFRunLoopAddSource(CFRunLoopGetCurrent(), impl_->runLoopSource, kCFRunLoopCommonModes); + + // Enable the event tap + CGEventTapEnable(impl_->eventTap, true); +} + +void KeyboardMonitor::Stop() { + if (impl_->eventTap == nullptr) { + return; // Already stopped + } + + // Disable the event tap + CGEventTapEnable(impl_->eventTap, false); + + // Remove from run loop + if (impl_->runLoopSource != nullptr) { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), impl_->runLoopSource, kCFRunLoopCommonModes); + CFRelease(impl_->runLoopSource); + impl_->runLoopSource = nullptr; + } + + // Release the event tap + CFRelease(impl_->eventTap); + impl_->eventTap = nullptr; +} + +bool KeyboardMonitor::IsMonitoring() const { + return impl_->eventTap != nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/launch_at_login_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/launch_at_login_macos.mm new file mode 100644 index 0000000..53d6c9a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/launch_at_login_macos.mm @@ -0,0 +1,322 @@ +#import +#if __has_include() +#import +#define NATIVEAPI_HAS_SM_APP_SERVICE 1 +#else +#define NATIVEAPI_HAS_SM_APP_SERVICE 0 +#endif +#include +#import +#include + +#include "../../launch_at_login.h" + +namespace nativeapi { + +namespace { + +// Convert std::string <-> NSString helpers +static inline NSString* ToNSString(const std::string& s) { + return [NSString stringWithUTF8String:s.c_str()]; +} + +static inline std::string ToStdString(NSString* s) { + if (!s) + return std::string(); + const char* cstr = [s UTF8String]; + return cstr ? std::string(cstr) : std::string(); +} + +// Best-effort default identifier: CFBundleIdentifier or +// "com.nativeapi.launch_at_login." +static std::string DetectDefaultId() { + @autoreleasepool { + NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier]; + if (bundleId.length > 0) { + return ToStdString(bundleId); + } + NSString* processName = [[NSProcessInfo processInfo] processName]; + if (processName.length == 0) { + processName = @"app"; + } + NSString* fallback = + [NSString stringWithFormat:@"com.nativeapi.launch_at_login.%@", processName]; + return ToStdString(fallback); + } +} + +// Best-effort default display name: CFBundleName or processName +static std::string DetectDefaultDisplayName() { + @autoreleasepool { + NSString* name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; + if (name.length == 0) { + name = [[NSProcessInfo processInfo] processName]; + } + if (name.length == 0) { + name = @"Application"; + } + return ToStdString(name); + } +} + +// Resolve current executable path. +// Prefer NSProcessInfo.arguments[0]; fallback to _NSGetExecutablePath. +static std::string DetectDefaultProgramPath() { + @autoreleasepool { + NSString* arg0 = [[[NSProcessInfo processInfo] arguments] firstObject]; + if (arg0.length > 0) { + // If arg0 is relative, attempt to get full path via file system resolution. + NSString* resolved = [arg0 stringByStandardizingPath]; + if (![resolved isAbsolutePath]) { + // Try to resolve via current directory (best-effort) + char cwdBuff[PATH_MAX]; + if (getcwd(cwdBuff, sizeof(cwdBuff))) { + NSString* cwd = [NSString stringWithUTF8String:cwdBuff]; + resolved = [cwd stringByAppendingPathComponent:arg0]; + resolved = [resolved stringByStandardizingPath]; + } + } + return ToStdString(resolved); + } + + // Fallback to _NSGetExecutablePath + uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + if (size > 0) { + std::string buffer(size + 1, '\0'); + if (_NSGetExecutablePath(buffer.data(), &size) == 0) { + return std::string(buffer.c_str()); + } + } + + return std::string(); + } +} + +static bool IsCurrentProgram(const std::string& executable_path) { + if (executable_path.empty()) { + return true; + } + + std::string current = DetectDefaultProgramPath(); + if (current.empty()) { + return false; + } + + @autoreleasepool { + NSString* requested = [ToNSString(executable_path) stringByStandardizingPath]; + NSString* detected = [ToNSString(current) stringByStandardizingPath]; + return [requested isEqualToString:detected]; + } +} + +} // namespace + +class LaunchAtLogin::Impl { + public: + static bool IsSupported() { +#if NATIVEAPI_HAS_SM_APP_SERVICE + if (@available(macOS 13.0, *)) { + return true; + } +#endif + return false; + } + + Impl() + : id_(DetectDefaultId()), + display_name_(DetectDefaultDisplayName()), + program_path_(DetectDefaultProgramPath()), + default_id_(id_), + default_program_path_(program_path_) {} + + explicit Impl(const std::string& id) + : id_(id), + display_name_(DetectDefaultDisplayName()), + program_path_(DetectDefaultProgramPath()), + default_id_(DetectDefaultId()), + default_program_path_(program_path_) {} + + Impl(const std::string& id, const std::string& display_name) + : id_(id), + display_name_(display_name), + program_path_(DetectDefaultProgramPath()), + default_id_(DetectDefaultId()), + default_program_path_(program_path_) {} + + ~Impl() = default; + + std::string GetId() const { return id_; } + + std::string GetDisplayName() const { return display_name_; } + + bool SetDisplayName(const std::string& display_name) { + display_name_ = display_name; + return true; + } + + bool SetProgram(const std::string& executable_path, const std::vector& arguments) { + program_path_ = executable_path; + arguments_ = arguments; + return true; + } + + std::string GetExecutablePath() const { return program_path_; } + + std::vector GetArguments() const { return arguments_; } + + bool Enable() { +#if NATIVEAPI_HAS_SM_APP_SERVICE + @autoreleasepool { + if (@available(macOS 13.0, *)) { + if (!CanUseConfiguredProgram()) { + return false; + } + + SMAppService* service = Service(); + if (!service) { + return false; + } + + SMAppServiceStatus status = service.status; + if (status == SMAppServiceStatusEnabled) { + return true; + } + if (status == SMAppServiceStatusRequiresApproval) { + return false; + } + + NSError* error = nil; + return [service registerAndReturnError:&error] == YES; + } + } +#endif + return false; + } + + bool Disable() { +#if NATIVEAPI_HAS_SM_APP_SERVICE + @autoreleasepool { + if (@available(macOS 13.0, *)) { + SMAppService* service = Service(); + if (!service) { + return false; + } + + if (service.status == SMAppServiceStatusNotRegistered) { + return true; + } + + NSError* error = nil; + return [service unregisterAndReturnError:&error] == YES || + service.status == SMAppServiceStatusNotRegistered; + } + } +#endif + return false; + } + + bool IsEnabled() const { +#if NATIVEAPI_HAS_SM_APP_SERVICE + @autoreleasepool { + if (@available(macOS 13.0, *)) { + SMAppService* service = Service(); + if (!service) { + return false; + } + + SMAppServiceStatus status = service.status; + return status == SMAppServiceStatusEnabled; + } + } +#endif + return false; + } + + private: +#if NATIVEAPI_HAS_SM_APP_SERVICE + SMAppService* Service() const API_AVAILABLE(macos(13.0)) { + @autoreleasepool { + if (id_.empty() || id_ == default_id_) { + return [SMAppService mainAppService]; + } + + NSString* identifier = ToNSString(id_); + if (identifier.length == 0) { + return nil; + } + return [SMAppService loginItemServiceWithIdentifier:identifier]; + } + } +#endif + + bool CanUseConfiguredProgram() const { + // SMAppService registers the main app or bundled helpers. It cannot register + // an arbitrary executable path or ProgramArguments like a legacy LaunchAgent. + return arguments_.empty() && + (program_path_.empty() || program_path_ == default_program_path_ || + IsCurrentProgram(program_path_)); + } + + private: + std::string id_; + std::string display_name_; + std::string program_path_; + std::vector arguments_; + std::string default_id_; + std::string default_program_path_; +}; + +// LaunchAtLogin public API implementations + +LaunchAtLogin::LaunchAtLogin() : pimpl_(std::make_unique()) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id) : pimpl_(std::make_unique(id)) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id, const std::string& display_name) + : pimpl_(std::make_unique(id, display_name)) {} + +LaunchAtLogin::~LaunchAtLogin() = default; + +bool LaunchAtLogin::IsSupported() { + return Impl::IsSupported(); +} + +std::string LaunchAtLogin::GetId() const { + return pimpl_->GetId(); +} + +std::string LaunchAtLogin::GetDisplayName() const { + return pimpl_->GetDisplayName(); +} + +bool LaunchAtLogin::SetDisplayName(const std::string& display_name) { + return pimpl_->SetDisplayName(display_name); +} + +bool LaunchAtLogin::SetProgram(const std::string& executable_path, + const std::vector& arguments) { + return pimpl_->SetProgram(executable_path, arguments); +} + +std::string LaunchAtLogin::GetExecutablePath() const { + return pimpl_->GetExecutablePath(); +} + +std::vector LaunchAtLogin::GetArguments() const { + return pimpl_->GetArguments(); +} + +bool LaunchAtLogin::Enable() { + return pimpl_->Enable(); +} + +bool LaunchAtLogin::Disable() { + return pimpl_->Disable(); +} + +bool LaunchAtLogin::IsEnabled() const { + return pimpl_->IsEnabled(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/menu_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/menu_macos.mm new file mode 100644 index 0000000..8e96395 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/menu_macos.mm @@ -0,0 +1,799 @@ +#include +#include +#include +#include +#include "../../foundation/id_allocator.h" +#include "../../image.h" +#include "../../menu.h" +#include "coordinate_utils_macos.h" + +// Import Cocoa headers +#import +#import + +// Note: This file assumes ARC (Automatic Reference Counting) is enabled +// for proper memory management of Objective-C objects. + +// Static keys for associated objects +static const void* kMenuItemIdKey = &kMenuItemIdKey; +static const void* kMenuIdKey = &kMenuIdKey; + +// Forward declarations - moved to global scope +typedef void (^MenuItemClickedBlock)(nativeapi::MenuItemId item_id); +typedef void (^MenuOpenedBlock)(nativeapi::MenuId menu_id); +typedef void (^MenuClosedBlock)(nativeapi::MenuId menu_id); + +@interface NSMenuItemTarget : NSObject +@property(nonatomic, copy) MenuItemClickedBlock clickedBlock; +- (void)menuItemClicked:(id)sender; +@end + +@interface NSMenuDelegateImpl : NSObject +@property(nonatomic, copy) MenuOpenedBlock openedBlock; +@property(nonatomic, copy) MenuClosedBlock closedBlock; +@end + +namespace nativeapi { + +// Removed global registries; events are dispatched via direct back-pointers + +// Helper function to convert KeyboardAccelerator to NSString and modifier mask +std::pair ConvertAccelerator(const KeyboardAccelerator& accelerator) { + NSString* key_equivalent = @""; + NSUInteger modifier_mask = 0; + + // Convert key + if (!accelerator.key.empty()) { + if (accelerator.key.length() == 1) { + // Single character key + char c = std::tolower(accelerator.key[0]); + key_equivalent = [NSString stringWithFormat:@"%c", c]; + } else { + // Special keys + std::string key = accelerator.key; + if (key == "F1") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF1FunctionKey]; + else if (key == "F2") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF2FunctionKey]; + else if (key == "F3") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF3FunctionKey]; + else if (key == "F4") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF4FunctionKey]; + else if (key == "F5") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF5FunctionKey]; + else if (key == "F6") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF6FunctionKey]; + else if (key == "F7") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF7FunctionKey]; + else if (key == "F8") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF8FunctionKey]; + else if (key == "F9") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF9FunctionKey]; + else if (key == "F10") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF10FunctionKey]; + else if (key == "F11") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF11FunctionKey]; + else if (key == "F12") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSF12FunctionKey]; + else if (key == "Enter" || key == "Return") + key_equivalent = @"\r"; + else if (key == "Tab") + key_equivalent = @"\t"; + else if (key == "Space") + key_equivalent = @" "; + else if (key == "Escape") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)0x1B]; + else if (key == "Delete" || key == "Backspace") + key_equivalent = @"\b"; + else if (key == "ArrowUp") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSUpArrowFunctionKey]; + else if (key == "ArrowDown") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSDownArrowFunctionKey]; + else if (key == "ArrowLeft") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSLeftArrowFunctionKey]; + else if (key == "ArrowRight") + key_equivalent = [NSString stringWithFormat:@"%C", (unichar)NSRightArrowFunctionKey]; + } + } + + // Convert modifiers + if ((accelerator.modifiers & ModifierKey::Ctrl) != ModifierKey::None) { + modifier_mask |= NSEventModifierFlagControl; + } + if ((accelerator.modifiers & ModifierKey::Alt) != ModifierKey::None) { + modifier_mask |= NSEventModifierFlagOption; + } + if ((accelerator.modifiers & ModifierKey::Shift) != ModifierKey::None) { + modifier_mask |= NSEventModifierFlagShift; + } + if ((accelerator.modifiers & ModifierKey::Meta) != ModifierKey::None) { + modifier_mask |= NSEventModifierFlagCommand; + } + + return std::make_pair(key_equivalent, modifier_mask); +} + +} // namespace nativeapi + +// Implementation of NSMenuItemTarget - moved to global scope +@implementation NSMenuItemTarget +- (void)menuItemClicked:(id)sender { + @try { + NSMenuItem* menu_item = (NSMenuItem*)sender; + if (!menu_item) + return; + + // Ignore clicks on disabled items as an extra safety net. + if (![menu_item isEnabled]) + return; + + // Call the block if it exists + if (_clickedBlock) { + // Get the MenuItemId from the menu item's associated object + NSNumber* item_id_obj = objc_getAssociatedObject(menu_item, kMenuItemIdKey); + if (item_id_obj) { + nativeapi::MenuItemId item_id = static_cast([item_id_obj longValue]); + _clickedBlock(item_id); + } + } + } @catch (NSException* exception) { + // Log the exception but don't crash + NSLog(@"Exception in menuItemClicked: %@", [exception reason]); + } +} + +@end + +// Implementation of NSMenuDelegateImpl - moved to global scope +@implementation NSMenuDelegateImpl +- (void)menuWillOpen:(NSMenu*)menu { + @try { + if (!menu) + return; + + if (_openedBlock) { + // Get the MenuId from the menu's associated object + NSNumber* menu_id_obj = objc_getAssociatedObject(menu, kMenuIdKey); + if (menu_id_obj) { + nativeapi::MenuId menu_id = static_cast([menu_id_obj longValue]); + _openedBlock(menu_id); + } + } + } @catch (NSException* exception) { + // Log the exception but don't crash + NSLog(@"Exception in menuWillOpen: %@", [exception reason]); + } +} + +- (BOOL)menu:(NSMenu*)menu validateMenuItem:(NSMenuItem*)item { + // Respect the programmatically set enabled state on NSMenuItem. + // Without this override, NSMenu's default autoenablesItems (YES) + // would re-enable all items whose target responds to the action, + // overriding explicit setEnabled:NO calls. + return [item isEnabled]; +} + +- (void)menuDidClose:(NSMenu*)menu { + @try { + if (!menu) + return; + + if (_closedBlock) { + // Get the MenuId from the menu's associated object + NSNumber* menu_id_obj = objc_getAssociatedObject(menu, kMenuIdKey); + if (menu_id_obj) { + nativeapi::MenuId menu_id = static_cast([menu_id_obj longValue]); + _closedBlock(menu_id); + } + } + } @catch (NSException* exception) { + // Log the exception but don't crash + NSLog(@"Exception in menuDidClose: %@", [exception reason]); + } +} +@end + +namespace nativeapi { + +// MenuItem::Impl implementation +class MenuItem::Impl { + public: + MenuItemId id_; + NSMenuItem* ns_menu_item_; + NSMenuItemTarget* ns_menu_item_target_; + MenuItemType type_; + std::optional label_; + std::shared_ptr image_; + std::optional tooltip_; + KeyboardAccelerator accelerator_; + bool has_accelerator_; + MenuItemState state_; + int radio_group_; + std::shared_ptr submenu_; + size_t submenu_opened_listener_id_; + size_t submenu_closed_listener_id_; + + Impl(MenuItemId id, NSMenuItem* menu_item, MenuItemType type) + : id_(id), + ns_menu_item_(menu_item), + ns_menu_item_target_([[NSMenuItemTarget alloc] init]), + type_(type), + accelerator_("", ModifierKey::None), + has_accelerator_(false), + state_(MenuItemState::Unchecked), + radio_group_(-1), + submenu_opened_listener_id_(0), + submenu_closed_listener_id_(0) { + [ns_menu_item_ setTarget:ns_menu_item_target_]; + [ns_menu_item_ setAction:@selector(menuItemClicked:)]; + } + + ~Impl() { + // First, remove submenu listeners before cleaning up submenu reference + if (submenu_ && submenu_opened_listener_id_ != 0) { + submenu_->RemoveListener(submenu_opened_listener_id_); + submenu_opened_listener_id_ = 0; + } + if (submenu_ && submenu_closed_listener_id_ != 0) { + submenu_->RemoveListener(submenu_closed_listener_id_); + submenu_closed_listener_id_ = 0; + } + + // Then clean up submenu reference + if (submenu_) { + submenu_.reset(); + } + + if (ns_menu_item_target_) { + // Clean up blocks first + ns_menu_item_target_.clickedBlock = nil; + + // Remove target and action to prevent callbacks after destruction + [ns_menu_item_ setTarget:nil]; + [ns_menu_item_ setAction:nil]; + ns_menu_item_target_ = nil; + } + } +}; + +// MenuItem implementation +MenuItem::MenuItem(const std::string& label, MenuItemType type) { + MenuItemId id = IdAllocator::Allocate(); + NSMenuItem* ns_item = nullptr; + + switch (type) { + case MenuItemType::Separator: + ns_item = [NSMenuItem separatorItem]; + break; + case MenuItemType::Normal: + case MenuItemType::Checkbox: + case MenuItemType::Radio: + case MenuItemType::Submenu: + default: + ns_item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label.c_str()] + action:nil + keyEquivalent:@""]; + break; + } + + pimpl_ = std::make_unique(id, ns_item, type); + objc_setAssociatedObject(ns_item, kMenuItemIdKey, [NSNumber numberWithUnsignedInt:id], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + pimpl_->label_ = label.empty() ? std::nullopt : std::optional(label); + + // 设置默认的 Block 处理器,直接发送事件 + pimpl_->ns_menu_item_target_.clickedBlock = ^(MenuItemId item_id) { + Emit(item_id); + }; +} + +MenuItem::MenuItem(void* native_item) { + MenuItemId id = IdAllocator::Allocate(); + NSMenuItem* ns_item = (__bridge NSMenuItem*)native_item; + pimpl_ = std::make_unique(id, (__bridge NSMenuItem*)native_item, MenuItemType::Normal); + objc_setAssociatedObject(ns_item, kMenuItemIdKey, [NSNumber numberWithUnsignedInt:id], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // 设置默认的 Block 处理器,直接发送事件 + pimpl_->ns_menu_item_target_.clickedBlock = ^(MenuItemId item_id) { + Emit(item_id); + }; +} + +MenuItem::~MenuItem() {} + +MenuItemId MenuItem::GetId() const { + return pimpl_->id_; +} + +MenuItemType MenuItem::GetType() const { + return pimpl_->type_; +} + +void MenuItem::SetLabel(const std::optional& label) { + pimpl_->label_ = label; + if (label.has_value()) { + [pimpl_->ns_menu_item_ setTitle:[NSString stringWithUTF8String:label->c_str()]]; + } else { + [pimpl_->ns_menu_item_ setTitle:@""]; + } +} + +std::optional MenuItem::GetLabel() const { + return pimpl_->label_; +} + +void MenuItem::SetIcon(std::shared_ptr image) { + pimpl_->image_ = image; + + NSImage* ns_image = nil; + + if (image) { + // Get NSImage directly from Image object using GetNativeObject + ns_image = (__bridge NSImage*)image->GetNativeObject(); + } + + if (ns_image) { + [ns_image setSize:NSMakeSize(16, 16)]; // Standard menu item icon size + [ns_image setTemplate:YES]; + [pimpl_->ns_menu_item_ setImage:ns_image]; + } else { + // Clear the image if no valid icon is provided + [pimpl_->ns_menu_item_ setImage:nil]; + } +} + +std::shared_ptr MenuItem::GetIcon() const { + return pimpl_->image_; +} + +void MenuItem::SetTooltip(const std::optional& tooltip) { + pimpl_->tooltip_ = tooltip; + if (tooltip.has_value()) { + [pimpl_->ns_menu_item_ setToolTip:[NSString stringWithUTF8String:tooltip->c_str()]]; + } else { + [pimpl_->ns_menu_item_ setToolTip:nil]; + } +} + +std::optional MenuItem::GetTooltip() const { + return pimpl_->tooltip_; +} + +void MenuItem::SetAccelerator(const std::optional& accelerator) { + if (accelerator.has_value()) { + pimpl_->accelerator_ = *accelerator; + pimpl_->has_accelerator_ = true; + + auto key_and_modifier = ConvertAccelerator(*accelerator); + [pimpl_->ns_menu_item_ setKeyEquivalent:key_and_modifier.first]; + [pimpl_->ns_menu_item_ setKeyEquivalentModifierMask:key_and_modifier.second]; + } else { + pimpl_->has_accelerator_ = false; + pimpl_->accelerator_ = KeyboardAccelerator("", ModifierKey::None); + [pimpl_->ns_menu_item_ setKeyEquivalent:@""]; + [pimpl_->ns_menu_item_ setKeyEquivalentModifierMask:0]; + } +} + +KeyboardAccelerator MenuItem::GetAccelerator() const { + if (pimpl_->has_accelerator_) { + return pimpl_->accelerator_; + } + return KeyboardAccelerator("", ModifierKey::None); +} + +void MenuItem::SetEnabled(bool enabled) { + [pimpl_->ns_menu_item_ setEnabled:enabled]; +} + +bool MenuItem::IsEnabled() const { + return [pimpl_->ns_menu_item_ isEnabled]; +} + +void MenuItem::SetState(MenuItemState state) { + if (pimpl_->type_ == MenuItemType::Checkbox || pimpl_->type_ == MenuItemType::Radio) { + // Radio buttons don't support Mixed state + if (pimpl_->type_ == MenuItemType::Radio && state == MenuItemState::Mixed) { + return; + } + + pimpl_->state_ = state; + + // Set the appropriate NSControlStateValue + NSControlStateValue ns_state; + switch (state) { + case MenuItemState::Unchecked: + ns_state = NSControlStateValueOff; + break; + case MenuItemState::Checked: + ns_state = NSControlStateValueOn; + break; + case MenuItemState::Mixed: + ns_state = NSControlStateValueMixed; + break; + } + [pimpl_->ns_menu_item_ setState:ns_state]; + + // Handle radio button group logic - uncheck siblings in the same NSMenu + if (pimpl_->type_ == MenuItemType::Radio && state == MenuItemState::Checked && + pimpl_->radio_group_ >= 0) { + NSMenu* parent_menu = [pimpl_->ns_menu_item_ menu]; + if (parent_menu) { + for (NSMenuItem* sibling in [parent_menu itemArray]) { + if (sibling == pimpl_->ns_menu_item_) + continue; + NSObject* target_obj = [sibling target]; + if ([target_obj isKindOfClass:[NSMenuItemTarget class]]) { + // Get the MenuItemId from the associated object + NSNumber* sibling_id_obj = objc_getAssociatedObject(sibling, kMenuItemIdKey); + if (sibling_id_obj) { + // Find the corresponding MenuItem in the parent menu's items + // This is a simplified approach - in practice, you might need to store + // a reference to the parent menu or use a different strategy + [sibling setState:NSControlStateValueOff]; + } + } + } + } + } + } +} + +MenuItemState MenuItem::GetState() const { + return pimpl_->state_; +} + +void MenuItem::SetRadioGroup(int group_id) { + pimpl_->radio_group_ = group_id; +} + +int MenuItem::GetRadioGroup() const { + return pimpl_->radio_group_; +} + +void MenuItem::SetSubmenu(std::shared_ptr submenu) { + try { + pimpl_->submenu_ = submenu; + if (submenu) { + NSMenu* ns_submenu = (__bridge NSMenu*)submenu->GetNativeObject(); + if (ns_submenu) { + [pimpl_->ns_menu_item_ setSubmenu:ns_submenu]; + + // Remove previous submenu listeners if they exist + if (pimpl_->submenu_opened_listener_id_ != 0) { + submenu->RemoveListener(pimpl_->submenu_opened_listener_id_); + pimpl_->submenu_opened_listener_id_ = 0; + } + if (pimpl_->submenu_closed_listener_id_ != 0) { + submenu->RemoveListener(pimpl_->submenu_closed_listener_id_); + pimpl_->submenu_closed_listener_id_ = 0; + } + + // Add event listeners to forward submenu events + MenuItemId menu_item_id = pimpl_->id_; + MenuItem* self = this; + pimpl_->submenu_opened_listener_id_ = submenu->AddListener( + [self, menu_item_id](const MenuOpenedEvent& event) { + self->Emit(menu_item_id); + }); + + pimpl_->submenu_closed_listener_id_ = submenu->AddListener( + [self, menu_item_id](const MenuClosedEvent& event) { + self->Emit(menu_item_id); + }); + } + } else { + // Remove listeners when submenu is cleared + if (pimpl_->submenu_ && pimpl_->submenu_opened_listener_id_ != 0) { + pimpl_->submenu_->RemoveListener(pimpl_->submenu_opened_listener_id_); + pimpl_->submenu_opened_listener_id_ = 0; + } + if (pimpl_->submenu_ && pimpl_->submenu_closed_listener_id_ != 0) { + pimpl_->submenu_->RemoveListener(pimpl_->submenu_closed_listener_id_); + pimpl_->submenu_closed_listener_id_ = 0; + } + [pimpl_->ns_menu_item_ setSubmenu:nil]; + } + } catch (...) { + // Handle C++ exceptions + NSLog(@"Exception in SetSubmenu"); + } +} + +std::shared_ptr MenuItem::GetSubmenu() const { + return pimpl_->submenu_; +} + +void* MenuItem::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ns_menu_item_; +} + +// Menu::Impl implementation +class Menu::Impl { + public: + MenuId id_; + NSMenu* ns_menu_; + NSMenuDelegateImpl* delegate_; + std::vector> items_; + + Impl(MenuId id, NSMenu* menu) + : id_(id), ns_menu_(menu), delegate_([[NSMenuDelegateImpl alloc] init]) { + [ns_menu_ setDelegate:delegate_]; + // Disable auto-enabling so explicit setEnabled: calls are respected. + [ns_menu_ setAutoenablesItems:NO]; + } + + ~Impl() { + // First, remove delegate to prevent callbacks during cleanup + if (delegate_) { + // Clean up blocks first + delegate_.openedBlock = nil; + delegate_.closedBlock = nil; + + [ns_menu_ setDelegate:nil]; + delegate_ = nil; + } + + // Then clear all menu item references + items_.clear(); + } +}; + +// Menu implementation +Menu::Menu() : Menu(nullptr) {} + +Menu::Menu(void* native_menu) { + MenuId id = IdAllocator::Allocate(); + NSMenu* ns_menu = nullptr; + + if (native_menu == nullptr) { + // Create new platform object + ns_menu = [[NSMenu alloc] init]; + } else { + // Wrap existing platform object + ns_menu = (__bridge NSMenu*)native_menu; + } + + // All initialization logic in one place + pimpl_ = std::make_unique(id, ns_menu); + objc_setAssociatedObject(ns_menu, kMenuIdKey, [NSNumber numberWithUnsignedInt:id], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // 设置默认的 Block 处理器,直接发送事件 + pimpl_->delegate_.openedBlock = ^(MenuId menu_id) { + Emit(menu_id); + }; + + pimpl_->delegate_.closedBlock = ^(MenuId menu_id) { + Emit(menu_id); + }; +} + +Menu::~Menu() {} + +MenuId Menu::GetId() const { + return pimpl_->id_; +} + +void Menu::AddItem(std::shared_ptr item) { + if (!item) + return; + + pimpl_->items_.push_back(item); + [pimpl_->ns_menu_ addItem:(__bridge NSMenuItem*)item->GetNativeObject()]; +} + +void Menu::InsertItem(size_t index, std::shared_ptr item) { + if (!item) + return; + + if (index >= pimpl_->items_.size()) { + AddItem(item); + return; + } + + pimpl_->items_.insert(pimpl_->items_.begin() + index, item); + [pimpl_->ns_menu_ insertItem:(__bridge NSMenuItem*)item->GetNativeObject() atIndex:index]; +} + +bool Menu::RemoveItem(std::shared_ptr item) { + if (!item) + return false; + + auto it = std::find(pimpl_->items_.begin(), pimpl_->items_.end(), item); + if (it != pimpl_->items_.end()) { + [pimpl_->ns_menu_ removeItem:(__bridge NSMenuItem*)item->GetNativeObject()]; + pimpl_->items_.erase(it); + return true; + } + return false; +} + +bool Menu::RemoveItemById(MenuItemId item_id) { + for (auto it = pimpl_->items_.begin(); it != pimpl_->items_.end(); ++it) { + if ((*it)->GetId() == item_id) { + [pimpl_->ns_menu_ removeItem:(__bridge NSMenuItem*)(*it)->GetNativeObject()]; + pimpl_->items_.erase(it); + return true; + } + } + return false; +} + +bool Menu::RemoveItemAt(size_t index) { + if (index >= pimpl_->items_.size()) + return false; + + auto item = pimpl_->items_[index]; + [pimpl_->ns_menu_ removeItem:(__bridge NSMenuItem*)item->GetNativeObject()]; + pimpl_->items_.erase(pimpl_->items_.begin() + index); + return true; +} + +void Menu::Clear() { + [pimpl_->ns_menu_ removeAllItems]; + pimpl_->items_.clear(); +} + +void Menu::AddSeparator() { + auto separator = std::make_shared("", MenuItemType::Separator); + AddItem(separator); +} + +void Menu::InsertSeparator(size_t index) { + auto separator = std::make_shared("", MenuItemType::Separator); + InsertItem(index, separator); +} + +size_t Menu::GetItemCount() const { + return pimpl_->items_.size(); +} + +std::shared_ptr Menu::GetItemAt(size_t index) const { + if (index >= pimpl_->items_.size()) + return nullptr; + return pimpl_->items_[index]; +} + +std::shared_ptr Menu::GetItemById(MenuItemId item_id) const { + for (const auto& item : pimpl_->items_) { + if (item->GetId() == item_id) { + return item; + } + } + return nullptr; +} + +std::vector> Menu::GetAllItems() const { + return pimpl_->items_; +} + +bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { + double x = 0, y = 0; + + // Determine position based on strategy type + switch (strategy.GetType()) { + case PositioningStrategy::Type::Absolute: + x = strategy.GetAbsolutePosition().x; + y = strategy.GetAbsolutePosition().y; + break; + + case PositioningStrategy::Type::CursorPosition: { + NSPoint mouse_location = NSPointExt::topLeft([NSEvent mouseLocation]); + x = mouse_location.x; + y = mouse_location.y; + break; + } + + case PositioningStrategy::Type::Relative: { + Rectangle rect = strategy.GetRelativeRectangle(); + Point offset = strategy.GetRelativeOffset(); + // Position at top-left corner of rectangle plus offset + x = rect.x + offset.x; + y = rect.y + offset.y; + break; + } + } + + // Get menu size for placement adjustments + NSSize menu_size = [pimpl_->ns_menu_ size]; + double menu_width = menu_size.width; + double menu_height = menu_size.height; + + // Adjust position based on placement + // Note: Coordinates are in top-left origin system (y grows downward) + // popUpMenuPositioningItem places the menu's top-left corner at the specified location + switch (placement) { + case Placement::TopStart: // Menu above reference point, left-aligned + // Menu's bottom-left corner at reference point + // No x adjustment needed (left-aligned) + // Move up by menu height + y -= menu_height; + break; + + case Placement::Top: // Menu above reference point, center-aligned + // Menu's bottom-center at reference point + x -= menu_width / 2.0; + y -= menu_height; + break; + + case Placement::TopEnd: // Menu above reference point, right-aligned + // Menu's bottom-right corner at reference point + x -= menu_width; + y -= menu_height; + break; + + case Placement::RightStart: // Menu to the right, top-aligned + // Menu's top-left corner at reference point (no adjustment needed) + break; + + case Placement::Right: // Menu to the right, center-aligned + // Menu's left-center at reference point + y -= menu_height / 2.0; + break; + + case Placement::RightEnd: // Menu to the right, bottom-aligned + // Menu's bottom-left corner at reference point + y -= menu_height; + break; + + case Placement::BottomStart: // Menu below reference point, left-aligned + // Menu's top-left corner at reference point (no adjustment needed) + break; + + case Placement::Bottom: // Menu below reference point, center-aligned + // Menu's top-center at reference point + x -= menu_width / 2.0; + break; + + case Placement::BottomEnd: // Menu below reference point, right-aligned + // Menu's top-right corner at reference point + x -= menu_width; + break; + + case Placement::LeftStart: // Menu to the left, top-aligned + // Menu's top-right corner at reference point + x -= menu_width; + break; + + case Placement::Left: // Menu to the left, center-aligned + // Menu's right-center at reference point + x -= menu_width; + y -= menu_height / 2.0; + break; + + case Placement::LeftEnd: // Menu to the left, bottom-aligned + // Menu's bottom-right corner at reference point + x -= menu_width; + y -= menu_height; + break; + } + + // Convert coordinates from top-left origin to macOS screen coordinates (bottom-left origin) + // macOS screen coordinates: origin at bottom-left, y grows upward + // Our coordinates: origin at top-left, y grows downward + CGPoint top_left_point = CGPointMake(x, y); + NSPoint point = NSPointExt::bottomLeft(top_left_point); + + // Use dispatch to ensure menu popup happens on the main run loop + // Show the menu using screen coordinates (inView:nil) + dispatch_async(dispatch_get_main_queue(), ^{ + @autoreleasepool { + [pimpl_->ns_menu_ popUpMenuPositioningItem:nil atLocation:point inView:nil]; + } + }); + + return true; +} + +bool Menu::Close() { + [pimpl_->ns_menu_ cancelTracking]; + return true; +} + +void* Menu::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ns_menu_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/message_dialog_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/message_dialog_macos.mm new file mode 100644 index 0000000..ba9178c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/message_dialog_macos.mm @@ -0,0 +1,171 @@ +#import +#include "../../dialog.h" +#include "../../message_dialog.h" + +namespace nativeapi { + +// Private implementation class for MessageDialog +class MessageDialog::Impl { + public: + Impl(const std::string& title, const std::string& message) + : title_(title), message_(message), ns_alert_(nil), is_open_(false) { + // Create NSAlert instance + ns_alert_ = [[NSAlert alloc] init]; + + // Set default values + [ns_alert_ setMessageText:[NSString stringWithUTF8String:title.c_str()]]; + [ns_alert_ setInformativeText:[NSString stringWithUTF8String:message.c_str()]]; + + // Set default alert style to informational + [ns_alert_ setAlertStyle:NSAlertStyleInformational]; + } + + ~Impl() { + if (ns_alert_) { + ns_alert_ = nil; + } + } + + void SetTitle(const std::string& title) { + title_ = title; + if (ns_alert_) { + [ns_alert_ setMessageText:[NSString stringWithUTF8String:title.c_str()]]; + } + } + + std::string GetTitle() const { return title_; } + + void SetMessage(const std::string& message) { + message_ = message; + if (ns_alert_) { + [ns_alert_ setInformativeText:[NSString stringWithUTF8String:message.c_str()]]; + } + } + + std::string GetMessage() const { return message_; } + + bool Open(DialogModality modality) { + if (!ns_alert_) { + return false; + } + + // Ensure we're on the main thread for UI operations + if (![NSThread isMainThread]) { + __block bool result = false; + dispatch_sync(dispatch_get_main_queue(), ^{ + result = OpenOnMainThread(modality); + }); + return result; + } + + return OpenOnMainThread(modality); + } + + bool OpenOnMainThread(DialogModality modality) { + if (!ns_alert_) { + return false; + } + + // Configure alert style based on modality + // Note: macOS doesn't have true system modal dialogs in modern versions + // Application is the standard modal behavior + // Window behaves as Application on macOS + switch (modality) { + case DialogModality::None: + // Non-modal: show as sheet or window that doesn't block + // For NSAlert, we can use beginSheetModalForWindow:completionHandler: + // However, NSAlert doesn't have a direct non-modal display method + // We'll use runModal with a workaround, or implement as sheet + // For now, treat None as non-blocking modal (temporary solution) + is_open_ = true; + @autoreleasepool { + // Note: NSAlert doesn't have a true non-modal mode + // This is a limitation of NSAlert API + // Consider using NSPanel or custom window for true non-modal dialogs + [ns_alert_ runModal]; + } + is_open_ = false; + break; + case DialogModality::Application: + case DialogModality::Window: + // Both use application modal behavior on macOS + // Run the alert modally - this blocks until the user dismisses the dialog + is_open_ = true; + @autoreleasepool { + [ns_alert_ runModal]; + } + // After runModal returns, the dialog has been dismissed + is_open_ = false; + break; + } + + return true; + } + + bool Close() { + if (!ns_alert_ || !is_open_) { + return false; + } + + // NSAlert doesn't have a direct close method + // We need to stop the modal session if it's running + // For sheet-based dialogs, we can dismiss the sheet + // Note: This is a limitation of NSAlert API + // In practice, the user must dismiss the dialog manually + + is_open_ = false; + return true; + } + + bool IsOpen() const { return is_open_; } + + private: + std::string title_; + std::string message_; + NSAlert* ns_alert_; + bool is_open_; +}; + +// MessageDialog implementation +MessageDialog::MessageDialog(const std::string& title, const std::string& message) + : pimpl_(std::make_unique(title, message)) { + // Set default modality to None (non-modal) + SetModality(DialogModality::None); +} + +MessageDialog::~MessageDialog() = default; + +void MessageDialog::SetTitle(const std::string& title) { + pimpl_->SetTitle(title); +} + +std::string MessageDialog::GetTitle() const { + return pimpl_->GetTitle(); +} + +void MessageDialog::SetMessage(const std::string& message) { + pimpl_->SetMessage(message); +} + +std::string MessageDialog::GetMessage() const { + return pimpl_->GetMessage(); +} + +DialogModality MessageDialog::GetModality() const { + return modality_; +} + +void MessageDialog::SetModality(DialogModality modality) { + modality_ = modality; +} + +bool MessageDialog::Open() { + DialogModality modality = GetModality(); + return pimpl_->Open(modality); +} + +bool MessageDialog::Close() { + return pimpl_->Close(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/preferences_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/preferences_macos.mm new file mode 100644 index 0000000..5b8c8ec --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/preferences_macos.mm @@ -0,0 +1,167 @@ +#import +#include "../../preferences.h" + +namespace nativeapi { + +class Preferences::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Create suite name for NSUserDefaults + NSString* suite_name = + [NSString stringWithFormat:@"com.nativeapi.preferences.%s", scope.c_str()]; + user_defaults_ = [[NSUserDefaults alloc] initWithSuiteName:suite_name]; + + if (!user_defaults_) { + // Fallback to standard user defaults + user_defaults_ = [NSUserDefaults standardUserDefaults]; + } + } + + ~Impl() { user_defaults_ = nil; } + + bool Set(const std::string& key, const std::string& value) { + @autoreleasepool { + NSString* ns_key = [NSString stringWithUTF8String:key.c_str()]; + NSString* ns_value = [NSString stringWithUTF8String:value.c_str()]; + + [user_defaults_ setObject:ns_value forKey:ns_key]; + return [user_defaults_ synchronize]; + } + } + + std::string Get(const std::string& key, const std::string& default_value) const { + @autoreleasepool { + NSString* ns_key = [NSString stringWithUTF8String:key.c_str()]; + NSString* ns_value = [user_defaults_ stringForKey:ns_key]; + + if (ns_value) { + return std::string([ns_value UTF8String]); + } + + return default_value; + } + } + + bool Remove(const std::string& key) { + @autoreleasepool { + NSString* ns_key = [NSString stringWithUTF8String:key.c_str()]; + + if ([user_defaults_ objectForKey:ns_key]) { + [user_defaults_ removeObjectForKey:ns_key]; + return [user_defaults_ synchronize]; + } + + return false; + } + } + + bool Clear() { + @autoreleasepool { + NSDictionary* dict = [user_defaults_ dictionaryRepresentation]; + + for (NSString* key in dict) { + [user_defaults_ removeObjectForKey:key]; + } + + return [user_defaults_ synchronize]; + } + } + + bool Contains(const std::string& key) const { + @autoreleasepool { + NSString* ns_key = [NSString stringWithUTF8String:key.c_str()]; + return [user_defaults_ objectForKey:ns_key] != nil; + } + } + + std::vector GetKeys() const { + @autoreleasepool { + std::vector keys; + NSDictionary* dict = [user_defaults_ dictionaryRepresentation]; + + for (NSString* key in dict) { + keys.push_back(std::string([key UTF8String])); + } + + return keys; + } + } + + size_t GetSize() const { + @autoreleasepool { + NSDictionary* dict = [user_defaults_ dictionaryRepresentation]; + return [dict count]; + } + } + + std::map GetAll() const { + @autoreleasepool { + std::map result; + NSDictionary* dict = [user_defaults_ dictionaryRepresentation]; + + for (NSString* key in dict) { + id value = [dict objectForKey:key]; + + // Only include string values + if ([value isKindOfClass:[NSString class]]) { + NSString* string_value = (NSString*)value; + result[std::string([key UTF8String])] = std::string([string_value UTF8String]); + } + } + + return result; + } + } + + const std::string& GetScope() const { return scope_; } + + private: + std::string scope_; + NSUserDefaults* user_defaults_; +}; + +// Constructor implementations +Preferences::Preferences() : Preferences("default") {} + +Preferences::Preferences(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +Preferences::~Preferences() = default; + +// Interface implementation +bool Preferences::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string Preferences::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool Preferences::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool Preferences::Clear() { + return pimpl_->Clear(); +} + +bool Preferences::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector Preferences::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t Preferences::GetSize() const { + return pimpl_->GetSize(); +} + +std::map Preferences::GetAll() const { + return pimpl_->GetAll(); +} + +std::string Preferences::GetScope() const { + return pimpl_->GetScope(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/secure_storage_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/secure_storage_macos.mm new file mode 100644 index 0000000..9077089 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/secure_storage_macos.mm @@ -0,0 +1,108 @@ +#import +#include "../../secure_storage.h" + +namespace nativeapi { + +class SecureStorage::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + std::string GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +SecureStorage::SecureStorage() : SecureStorage("default") {} + +SecureStorage::SecureStorage(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +SecureStorage::~SecureStorage() = default; + +bool SecureStorage::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string SecureStorage::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool SecureStorage::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool SecureStorage::Clear() { + return pimpl_->Clear(); +} + +bool SecureStorage::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector SecureStorage::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t SecureStorage::GetSize() const { + return pimpl_->GetSize(); +} + +std::map SecureStorage::GetAll() const { + return pimpl_->GetAll(); +} + +std::string SecureStorage::GetScope() const { + return pimpl_->GetScope(); +} + +bool SecureStorage::IsAvailable() { + // Stub implementation - report as unavailable + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/shortcut_manager_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/shortcut_manager_macos.mm new file mode 100644 index 0000000..1db7b6a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/shortcut_manager_macos.mm @@ -0,0 +1,346 @@ +#include +#include +#include +#include +#include +#include + +#import + +#include "../../shortcut_manager.h" + +namespace nativeapi { +namespace { + +// Four-char signature tagging every hotkey this library owns, so the shared +// Carbon handler can ignore hotkeys registered by the host application. +constexpr FourCharCode kHotKeySignature = 'ntap'; + +std::string ShortcutToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + return value; +} + +std::vector SplitShortcutAccelerator(const std::string& accelerator) { + std::vector parts; + std::string current; + for (char ch : accelerator) { + if (ch == '+') { + if (!current.empty()) { + parts.push_back(current); + current.clear(); + } + } else if (!std::isspace(static_cast(ch))) { + current.push_back(ch); + } + } + if (!current.empty()) { + parts.push_back(current); + } + return parts; +} + +bool ParseShortcutAcceleratorTokens(const std::string& accelerator, + std::vector& modifiers, + std::string& key_token) { + modifiers.clear(); + key_token.clear(); + + auto parts = SplitShortcutAccelerator(accelerator); + if (parts.empty()) { + return false; + } + + for (auto& part : parts) { + std::string token = ShortcutToLower(part); + if (token == "ctrl" || token == "control" || token == "alt" || token == "option" || + token == "shift" || token == "cmd" || token == "command" || token == "super" || + token == "meta" || token == "cmdorctrl" || token == "commandorcontrol") { + modifiers.push_back(token); + } else { + if (!key_token.empty()) { + return false; + } + key_token = token; + } + } + + return !key_token.empty(); +} + +// Token -> Carbon virtual key code. +// +// Carbon's kVK_* constants are positional (they describe where the key sits on +// an ANSI board, not what it prints), and they are non-contiguous — so this has +// to be an explicit table rather than arithmetic on a base value. The coverage +// mirrors soffes/HotKey's Key enum: letters, digits, F1-F20, navigation, +// punctuation and the keypad. +bool LookupShortcutKeyCode(const std::string& token, UInt32& keycode) { + static const std::unordered_map kKeyCodes = { + // Letters. + {"a", kVK_ANSI_A}, {"b", kVK_ANSI_B}, {"c", kVK_ANSI_C}, {"d", kVK_ANSI_D}, + {"e", kVK_ANSI_E}, {"f", kVK_ANSI_F}, {"g", kVK_ANSI_G}, {"h", kVK_ANSI_H}, + {"i", kVK_ANSI_I}, {"j", kVK_ANSI_J}, {"k", kVK_ANSI_K}, {"l", kVK_ANSI_L}, + {"m", kVK_ANSI_M}, {"n", kVK_ANSI_N}, {"o", kVK_ANSI_O}, {"p", kVK_ANSI_P}, + {"q", kVK_ANSI_Q}, {"r", kVK_ANSI_R}, {"s", kVK_ANSI_S}, {"t", kVK_ANSI_T}, + {"u", kVK_ANSI_U}, {"v", kVK_ANSI_V}, {"w", kVK_ANSI_W}, {"x", kVK_ANSI_X}, + {"y", kVK_ANSI_Y}, {"z", kVK_ANSI_Z}, + + // Digits. + {"0", kVK_ANSI_0}, {"1", kVK_ANSI_1}, {"2", kVK_ANSI_2}, {"3", kVK_ANSI_3}, + {"4", kVK_ANSI_4}, {"5", kVK_ANSI_5}, {"6", kVK_ANSI_6}, {"7", kVK_ANSI_7}, + {"8", kVK_ANSI_8}, {"9", kVK_ANSI_9}, + + // Function keys. + {"f1", kVK_F1}, {"f2", kVK_F2}, {"f3", kVK_F3}, {"f4", kVK_F4}, + {"f5", kVK_F5}, {"f6", kVK_F6}, {"f7", kVK_F7}, {"f8", kVK_F8}, + {"f9", kVK_F9}, {"f10", kVK_F10}, {"f11", kVK_F11}, {"f12", kVK_F12}, + {"f13", kVK_F13}, {"f14", kVK_F14}, {"f15", kVK_F15}, {"f16", kVK_F16}, + {"f17", kVK_F17}, {"f18", kVK_F18}, {"f19", kVK_F19}, {"f20", kVK_F20}, + + // Whitespace and editing. + {"space", kVK_Space}, + {"tab", kVK_Tab}, + {"enter", kVK_Return}, + {"return", kVK_Return}, + {"escape", kVK_Escape}, + {"esc", kVK_Escape}, + {"backspace", kVK_Delete}, + {"delete", kVK_ForwardDelete}, + {"forwarddelete", kVK_ForwardDelete}, + {"insert", kVK_Help}, + {"help", kVK_Help}, + + // Navigation. + {"home", kVK_Home}, + {"end", kVK_End}, + {"pageup", kVK_PageUp}, + {"pagedown", kVK_PageDown}, + {"up", kVK_UpArrow}, + {"down", kVK_DownArrow}, + {"left", kVK_LeftArrow}, + {"right", kVK_RightArrow}, + + // Punctuation, by name and by literal character. + {"plus", kVK_ANSI_Equal}, + {"equal", kVK_ANSI_Equal}, {"=", kVK_ANSI_Equal}, + {"minus", kVK_ANSI_Minus}, {"-", kVK_ANSI_Minus}, + {"comma", kVK_ANSI_Comma}, {",", kVK_ANSI_Comma}, + {"period", kVK_ANSI_Period}, {".", kVK_ANSI_Period}, + {"slash", kVK_ANSI_Slash}, {"/", kVK_ANSI_Slash}, + {"backslash", kVK_ANSI_Backslash},{"\\", kVK_ANSI_Backslash}, + {"semicolon", kVK_ANSI_Semicolon},{";", kVK_ANSI_Semicolon}, + {"quote", kVK_ANSI_Quote}, {"'", kVK_ANSI_Quote}, + {"leftbracket", kVK_ANSI_LeftBracket}, {"[", kVK_ANSI_LeftBracket}, + {"rightbracket", kVK_ANSI_RightBracket}, {"]", kVK_ANSI_RightBracket}, + {"grave", kVK_ANSI_Grave}, {"backquote", kVK_ANSI_Grave}, {"`", kVK_ANSI_Grave}, + + // Keypad. + {"num0", kVK_ANSI_Keypad0}, {"num1", kVK_ANSI_Keypad1}, {"num2", kVK_ANSI_Keypad2}, + {"num3", kVK_ANSI_Keypad3}, {"num4", kVK_ANSI_Keypad4}, {"num5", kVK_ANSI_Keypad5}, + {"num6", kVK_ANSI_Keypad6}, {"num7", kVK_ANSI_Keypad7}, {"num8", kVK_ANSI_Keypad8}, + {"num9", kVK_ANSI_Keypad9}, + {"numdec", kVK_ANSI_KeypadDecimal}, + {"numadd", kVK_ANSI_KeypadPlus}, + {"numsub", kVK_ANSI_KeypadMinus}, + {"nummult", kVK_ANSI_KeypadMultiply}, + {"numdiv", kVK_ANSI_KeypadDivide}, + {"numenter", kVK_ANSI_KeypadEnter}, + }; + + auto it = kKeyCodes.find(token); + if (it == kKeyCodes.end()) { + return false; + } + keycode = it->second; + return true; +} + +bool ParseMacShortcutAccelerator(const std::string& accelerator, + UInt32& modifiers, + UInt32& keycode) { + modifiers = 0; + keycode = 0; + + std::vector modifier_tokens; + std::string key_token; + if (!ParseShortcutAcceleratorTokens(accelerator, modifier_tokens, key_token)) { + return false; + } + + for (const auto& token : modifier_tokens) { + if (token == "ctrl" || token == "control") { + modifiers |= controlKey; + } else if (token == "alt" || token == "option") { + modifiers |= optionKey; + } else if (token == "shift") { + modifiers |= shiftKey; + } else { + // cmd / command / meta / super / cmdorctrl all mean Command on macOS. + modifiers |= cmdKey; + } + } + + return LookupShortcutKeyCode(key_token, keycode); +} + +} // namespace + +/** + * @brief macOS global shortcuts, built on Carbon's RegisterEventHotKey. + * + * Modelled on soffes/HotKey: one process-wide Carbon event handler, one + * RegisterEventHotKey call per shortcut, and an EventHotKeyID whose signature + * identifies hotkeys this library owns. + * + * @note Carbon delivers hotkeys into the *main thread's* event queue, so + * something must pump that queue. A Cocoa app gets this from + * `[NSApp run]`; a program without one calls RunMainThreadLoopFor() + * (see PlatformRunMainThreadLoopFor in dispatcher_macos.mm, which + * services the Carbon queue as well as the GCD main queue). Nothing + * is delivered while no one pumps — an earlier revision of this file + * tried to pump from a private background thread, which cannot work: + * ReceiveNextEvent() drains the *calling* thread's queue, and hotkey + * events are never posted to a worker thread's queue. + */ +class ShortcutManagerImpl final : public ShortcutManager::Impl { + public: + explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) {} + + ~ShortcutManagerImpl() override { + std::lock_guard lock(mutex_); + for (const auto& [id, hotkey] : hotkeys_) { + UnregisterEventHotKey(hotkey); + } + hotkeys_.clear(); + + if (handler_) { + RemoveEventHandler(handler_); + handler_ = nullptr; + } + } + + bool IsSupported() override { return true; } + + bool RegisterShortcut(const std::shared_ptr& shortcut) override { + UInt32 modifiers = 0; + UInt32 keycode = 0; + if (!ParseMacShortcutAccelerator(shortcut->GetAccelerator(), modifiers, keycode)) { + return false; + } + + EnsureHandler(); + + EventHotKeyID hotkey_id; + hotkey_id.signature = kHotKeySignature; + hotkey_id.id = static_cast(shortcut->GetId()); + + EventHotKeyRef hotkey_ref = nullptr; + // GetApplicationEventTarget() rather than soffes/HotKey's + // GetEventDispatcherTarget(): the dispatcher target is per-thread, and this + // library documents Register() as callable from any thread. The application + // target is process-wide, and the main thread's dispatcher propagates to it, + // so delivery works under both a Cocoa run loop and RunMainThreadLoopFor(). + OSStatus status = RegisterEventHotKey(keycode, modifiers, hotkey_id, + GetApplicationEventTarget(), 0, &hotkey_ref); + if (status != noErr || !hotkey_ref) { + return false; + } + + std::lock_guard lock(mutex_); + hotkeys_[shortcut->GetId()] = hotkey_ref; + return true; + } + + bool UnregisterShortcut(const std::shared_ptr& shortcut) override { + EventHotKeyRef hotkey_ref = nullptr; + { + std::lock_guard lock(mutex_); + auto it = hotkeys_.find(shortcut->GetId()); + if (it == hotkeys_.end()) { + return false; + } + hotkey_ref = it->second; + hotkeys_.erase(it); + } + + UnregisterEventHotKey(hotkey_ref); + return true; + } + + void SetupEventMonitoring() override { EnsureHandler(); } + + void CleanupEventMonitoring() override { + // The handler is shared with the shortcuts themselves, which outlive any + // individual event listener; it is torn down in the destructor instead. + } + + private: + static OSStatus HotKeyHandler(EventHandlerCallRef next_handler, EventRef event, void* user_data) { + auto* self = static_cast(user_data); + if (!self) { + return eventNotHandledErr; + } + + EventHotKeyID hotkey_id; + OSStatus status = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, nullptr, + sizeof(EventHotKeyID), nullptr, &hotkey_id); + if (status != noErr) { + return eventNotHandledErr; + } + + // Leave hotkeys owned by the host application to the host's own handlers. + if (hotkey_id.signature != kHotKeySignature) { + return eventNotHandledErr; + } + + self->HandleHotKey(static_cast(hotkey_id.id)); + return noErr; + } + + void EnsureHandler() { + std::lock_guard lock(handler_mutex_); + if (handler_) { + return; + } + + EventTypeSpec event_type; + event_type.eventClass = kEventClassKeyboard; + event_type.eventKind = kEventHotKeyPressed; + + InstallEventHandler(GetApplicationEventTarget(), HotKeyHandler, 1, &event_type, this, + &handler_); + } + + void HandleHotKey(ShortcutId shortcut_id) { + auto shortcut = manager_->Get(shortcut_id); + if (!shortcut) { + return; + } + + if (!manager_->IsEnabled() || !shortcut->IsEnabled()) { + return; + } + + manager_->EmitShortcutActivated(shortcut_id, shortcut->GetAccelerator()); + shortcut->Invoke(); + } + + ShortcutManager* manager_; + + std::mutex mutex_; + std::unordered_map hotkeys_; + + std::mutex handler_mutex_; + EventHandlerRef handler_ = nullptr; +}; + +ShortcutManager::ShortcutManager() + : pimpl_(std::make_unique(this)), next_shortcut_id_(1), enabled_(true) {} + +ShortcutManager::~ShortcutManager() { + UnregisterAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/tray_icon_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/tray_icon_macos.mm new file mode 100644 index 0000000..19cf4dd --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/tray_icon_macos.mm @@ -0,0 +1,428 @@ +#include +#include "../../foundation/geometry.h" +#include "../../image.h" +#include "../../menu.h" +#include "../../positioning_strategy.h" +#include "../../tray_icon.h" +#include "coordinate_utils_macos.h" + +#import +#import +#import + +// Note: This file assumes ARC (Automatic Reference Counting) is enabled +// for proper memory management of Objective-C objects. + +// Forward declarations +typedef void (^TrayIconClickedBlock)(void); +typedef void (^TrayIconRightClickedBlock)(void); +typedef void (^TrayIconDoubleClickedBlock)(void); + +// Key for associated object to store tray icon ID +static const void* kTrayIconIdKey = &kTrayIconIdKey; + +@interface NSStatusBarButtonTarget : NSObject +@property(nonatomic, copy) TrayIconClickedBlock left_clicked_callback_; +@property(nonatomic, copy) TrayIconRightClickedBlock right_clicked_callback_; +@property(nonatomic, copy) TrayIconDoubleClickedBlock double_clicked_callback_; +- (void)handleStatusItemEvent:(id)sender; +@end + +namespace nativeapi { + +// Private implementation class +class TrayIcon::Impl { + public: + std::shared_ptr image_; + + Impl(NSStatusItem* status_item) + : ns_status_item_(status_item), + ns_status_bar_button_target_(nil), + menu_closed_listener_id_(0), + click_handler_setup_(false), + context_menu_trigger_(ContextMenuTrigger::None) { + if (status_item) { + // Check if ID already exists in the associated object + NSNumber* allocated_id = objc_getAssociatedObject(status_item, kTrayIconIdKey); + if (allocated_id) { + // Reuse allocated ID + id_ = static_cast([allocated_id longValue]); + } else { + // Allocate new ID and store it + id_ = IdAllocator::Allocate(); + objc_setAssociatedObject(status_item, kTrayIconIdKey, [NSNumber numberWithLong:id_], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + } + } + + ~Impl() { + // Remove the menu closed listener before cleaning up + if (context_menu_ && menu_closed_listener_id_ != 0) { + context_menu_->RemoveListener(menu_closed_listener_id_); + menu_closed_listener_id_ = 0; + } + + // Clean up event handlers if they were set up + if (click_handler_setup_) { + CleanupEventHandlers(); + } + + // Then clean up the status item + if (ns_status_item_) { + // Clear menu reference + ns_status_item_.menu = nil; + + // Clean up associated object + objc_setAssociatedObject(ns_status_item_, kTrayIconIdKey, nil, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + [[NSStatusBar systemStatusBar] removeStatusItem:ns_status_item_]; + ns_status_item_ = nil; + } + + // Finally, safely clean up context_menu_ after all UI references are cleared + if (context_menu_) { + context_menu_.reset(); // Explicitly reset shared_ptr + } + } + + void SetupEventHandlers() { + if (click_handler_setup_) { + return; // Already set up + } + + if (!ns_status_item_ || !ns_status_item_.button) { + return; + } + + // Create and set up button target + ns_status_bar_button_target_ = [[NSStatusBarButtonTarget alloc] init]; + + // Set up event handlers + [ns_status_item_.button setTarget:ns_status_bar_button_target_]; + [ns_status_item_.button setAction:@selector(handleStatusItemEvent:)]; + + // Enable right-click handling + [ns_status_item_.button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp]; + + click_handler_setup_ = true; + } + + void CleanupEventHandlers() { + if (!click_handler_setup_) { + return; // Not set up + } + + // Clean up blocks first + if (ns_status_bar_button_target_) { + ns_status_bar_button_target_.left_clicked_callback_ = nil; + ns_status_bar_button_target_.right_clicked_callback_ = nil; + ns_status_bar_button_target_.double_clicked_callback_ = nil; + ns_status_bar_button_target_ = nil; + } + + // Remove target and action to prevent callbacks after destruction + if (ns_status_item_ && ns_status_item_.button) { + [ns_status_item_.button setTarget:nil]; + [ns_status_item_.button setAction:nil]; + } + + click_handler_setup_ = false; + } + + NSStatusItem* ns_status_item_; + NSStatusBarButtonTarget* ns_status_bar_button_target_; + + TrayIconId id_; + std::shared_ptr context_menu_; + size_t menu_closed_listener_id_; + bool click_handler_setup_; + ContextMenuTrigger context_menu_trigger_; +}; + +TrayIcon::TrayIcon() : TrayIcon(nullptr) {} + +TrayIcon::TrayIcon(void* tray) { + NSStatusItem* status_item = nullptr; + + if (tray == nullptr) { + // Create platform-specific NSStatusItem + NSStatusBar* status_bar = [NSStatusBar systemStatusBar]; + status_item = [status_bar statusItemWithLength:NSVariableStatusItemLength]; + } else { + status_item = (__bridge NSStatusItem*)tray; + } + + // Initialize the Impl with the status item + pimpl_ = std::make_unique(status_item); + + // Event handlers will be set up automatically when first listener is added + // via StartEventListening() override +} + +TrayIcon::~TrayIcon() = default; + +void TrayIcon::StartEventListening() { + // Called automatically when first listener is added + // Set up platform event monitoring + pimpl_->SetupEventHandlers(); + + // Set up click handler blocks + if (pimpl_->ns_status_bar_button_target_) { + pimpl_->ns_status_bar_button_target_.left_clicked_callback_ = ^{ + Emit(pimpl_->id_); + // Auto-trigger context menu if configured + if (pimpl_->context_menu_trigger_ == ContextMenuTrigger::Clicked) { + OpenContextMenu(); + } + }; + + pimpl_->ns_status_bar_button_target_.right_clicked_callback_ = ^{ + Emit(pimpl_->id_); + // Auto-trigger context menu if configured + if (pimpl_->context_menu_trigger_ == ContextMenuTrigger::RightClicked) { + OpenContextMenu(); + } + }; + + pimpl_->ns_status_bar_button_target_.double_clicked_callback_ = ^{ + Emit(pimpl_->id_); + // Auto-trigger context menu if configured + if (pimpl_->context_menu_trigger_ == ContextMenuTrigger::DoubleClicked) { + OpenContextMenu(); + } + }; + } +} + +void TrayIcon::StopEventListening() { + // Called automatically when last listener is removed + // Clean up platform event monitoring + pimpl_->CleanupEventHandlers(); +} + +TrayIconId TrayIcon::GetId() { + return pimpl_->id_; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + if (!pimpl_->ns_status_item_ || !pimpl_->ns_status_item_.button) { + return; + } + + // Store the image reference + pimpl_->image_ = image; + + NSImage* ns_image = nil; + + if (image) { + // Get NSImage directly from Image object using GetNativeObject + ns_image = (__bridge NSImage*)image->GetNativeObject(); + } + + if (ns_image) { + // Set appropriate size for status bar + [ns_image setSize:NSMakeSize(18, 18)]; + // Make it template image for proper appearance in dark mode + [ns_image setTemplate:YES]; + + // Set the image to the button + [pimpl_->ns_status_item_.button setImage:ns_image]; + } else { + // Clear the image if no valid icon is provided + [pimpl_->ns_status_item_.button setImage:nil]; + } +} + +std::shared_ptr TrayIcon::GetIcon() const { + return pimpl_->image_; +} + +void TrayIcon::SetTitle(std::optional title) { + if (pimpl_->ns_status_item_ && pimpl_->ns_status_item_.button) { + if (title.has_value()) { + NSString* title_string = [NSString stringWithUTF8String:title.value().c_str()]; + [pimpl_->ns_status_item_.button setTitle:title_string]; + } else { + [pimpl_->ns_status_item_.button setTitle:@""]; + } + } +} + +std::optional TrayIcon::GetTitle() { + if (pimpl_->ns_status_item_ && pimpl_->ns_status_item_.button) { + NSString* title_string = [pimpl_->ns_status_item_.button title]; + if (title_string && [title_string length] > 0) { + return std::string([title_string UTF8String]); + } + } + return std::nullopt; +} + +void TrayIcon::SetTooltip(std::optional tooltip) { + if (pimpl_->ns_status_item_ && pimpl_->ns_status_item_.button) { + if (tooltip.has_value()) { + NSString* tooltip_string = [NSString stringWithUTF8String:tooltip.value().c_str()]; + [pimpl_->ns_status_item_.button setToolTip:tooltip_string]; + } else { + [pimpl_->ns_status_item_.button setToolTip:nil]; + } + } +} + +std::optional TrayIcon::GetTooltip() { + if (pimpl_->ns_status_item_ && pimpl_->ns_status_item_.button) { + NSString* tooltip_string = [pimpl_->ns_status_item_.button toolTip]; + if (tooltip_string && [tooltip_string length] > 0) { + return std::string([tooltip_string UTF8String]); + } + } + return std::nullopt; +} + +void TrayIcon::SetContextMenu(std::shared_ptr menu) { + // Remove previous menu listener if it exists + if (pimpl_->context_menu_ && pimpl_->menu_closed_listener_id_ != 0) { + pimpl_->context_menu_->RemoveListener(pimpl_->menu_closed_listener_id_); + pimpl_->menu_closed_listener_id_ = 0; + } + + // Store the menu reference + // Don't set the menu directly to the status item, as this would cause + // macOS to take over click handling and prevent our custom click events + // Instead, we'll show the menu manually in our click handler + pimpl_->context_menu_ = menu; + + if (pimpl_->context_menu_) { + auto pimpl_raw = pimpl_.get(); + pimpl_->menu_closed_listener_id_ = pimpl_->context_menu_->AddListener( + [pimpl_raw](const MenuClosedEvent& event) { + if (pimpl_raw && pimpl_raw->ns_status_item_) { + pimpl_raw->ns_status_item_.menu = nil; + } + }); + } +} + +std::shared_ptr TrayIcon::GetContextMenu() { + return pimpl_->context_menu_; +} + +Rectangle TrayIcon::GetBounds() { + Rectangle bounds = {0, 0, 0, 0}; + + if (pimpl_->ns_status_item_ && pimpl_->ns_status_item_.button && pimpl_->ns_status_item_.button.window) { + NSStatusBarButton* button = pimpl_->ns_status_item_.button; + NSRect window_rect = [button convertRect:button.bounds toView:nil]; + NSRect screen_rect = [button.window convertRectToScreen:window_rect]; + + // Flip against the primary screen ([NSScreen screens][0]), matching every + // other coordinate conversion in this library. Do NOT use mainScreen here: + // it is the screen of the current key window, which makes the result depend + // on where keyboard focus happens to be on multi-display setups. + CGPoint top_left = NSRectExt::topLeft(screen_rect); + + bounds.x = top_left.x; + bounds.y = top_left.y; + bounds.width = screen_rect.size.width; + bounds.height = screen_rect.size.height; + } + + return bounds; +} + +bool TrayIcon::SetVisible(bool visible) { + if (!pimpl_->ns_status_item_) { + return false; + } + + [pimpl_->ns_status_item_ setVisible:visible ? YES : NO]; + return true; +} + +bool TrayIcon::IsVisible() { + if (pimpl_->ns_status_item_) { + return [pimpl_->ns_status_item_ isVisible] == YES; + } + return false; +} + +bool TrayIcon::OpenContextMenu() { + if (!pimpl_->context_menu_ || !pimpl_->ns_status_item_ || !pimpl_->ns_status_item_.button) { + return false; + } + + // Use the Swift approach: set menu to status item and simulate click + // Get the native NSMenu object from our Menu wrapper + NSMenu* native_menu = (__bridge NSMenu*)pimpl_->context_menu_->GetNativeObject(); + if (!native_menu) { + return false; + } + + // // Set our menu delegate to handle menu close events + // [nativeMenu setDelegate:pimpl_->menu_delegate_]; + + // Set the menu to the status item (like Swift version) + pimpl_->ns_status_item_.menu = native_menu; + + // Simulate a click to show the menu (like Swift version) + [pimpl_->ns_status_item_.button performClick:nil]; + + return true; +} + +bool TrayIcon::CloseContextMenu() { + if (!pimpl_->context_menu_) { + return true; // No menu to close, consider success + } + + // Close the context menu + return pimpl_->context_menu_->Close(); +} + +void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { + pimpl_->context_menu_trigger_ = trigger; +} + +ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { + return pimpl_->context_menu_trigger_; +} + +void* TrayIcon::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ns_status_item_; +} + +} // namespace nativeapi + +// Implementation of NSStatusBarButtonTarget +@implementation NSStatusBarButtonTarget + +- (void)handleStatusItemEvent:(id)sender { + NSEvent* event = [NSApp currentEvent]; + if (!event) + return; + + // Check the type of click and call appropriate block + if (event.type == NSEventTypeRightMouseUp || + (event.type == NSEventTypeLeftMouseUp && + (event.modifierFlags & NSEventModifierFlagControl))) { + // Right click or Ctrl+Left click + if (_right_clicked_callback_) { + _right_clicked_callback_(); + } + } else if (event.type == NSEventTypeLeftMouseUp) { + // Check for double click + if (event.clickCount == 2) { + if (_double_clicked_callback_) { + _double_clicked_callback_(); + } + } else { + if (_left_clicked_callback_) { + _left_clicked_callback_(); + } + } + } +} + +@end diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/tray_manager_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/tray_manager_macos.mm new file mode 100644 index 0000000..ba8c14a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/tray_manager_macos.mm @@ -0,0 +1,79 @@ +#include +#include +#include +#include + +#include "../../menu.h" +#include "../../tray_icon.h" +#include "../../tray_manager.h" + +// Import Cocoa headers +#import + +namespace nativeapi { + +class TrayManager::Impl { + public: + Impl() {} + ~Impl() {} +}; + +TrayManager::TrayManager() : next_tray_id_(1), pimpl_(std::make_unique()) {} + +TrayManager::~TrayManager() { + std::lock_guard lock(mutex_); + + // First, hide all tray icons to prevent further UI interactions + for (auto& pair : trays_) { + auto tray = pair.second; + if (tray) { + try { + tray->SetVisible(false); + } catch (...) { + // Ignore exceptions during cleanup + } + } + } + + // Then, clean up all tray icon menu references to prevent circular references + for (auto& pair : trays_) { + auto tray = pair.second; + if (tray) { + try { + // Explicitly clear menu references + tray->SetContextMenu(nullptr); + } catch (...) { + // Ignore exceptions during cleanup + } + } + } + + // Finally, clear the container + trays_.clear(); +} + +bool TrayManager::IsSupported() { + return true; +} + +std::shared_ptr TrayManager::Get(TrayIconId id) { + std::lock_guard lock(mutex_); + + auto it = trays_.find(id); + if (it != trays_.end()) { + return it->second; + } + return nullptr; +} + +std::vector> TrayManager::GetAll() { + std::lock_guard lock(mutex_); + + std::vector> trays; + for (const auto& pair : trays_) { + trays.push_back(pair.second); + } + return trays; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/url_opener_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/url_opener_macos.mm new file mode 100644 index 0000000..9ecef11 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/url_opener_macos.mm @@ -0,0 +1,56 @@ +#import +#import + +#include "../../url_opener.h" + +namespace nativeapi { +namespace { + +class MacosUrlOpenerImpl final : public UrlOpener::Impl { + public: + bool IsSupported() const override { return true; } + + UrlOpenResult Open(const std::string& url) const override { + @autoreleasepool { + NSString* ns_url = [NSString stringWithUTF8String:url.c_str()]; + if (!ns_url) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "Failed to build NSURL from UTF-8 input."; + return result; + } + + NSURL* target = [NSURL URLWithString:ns_url]; + if (!target) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "Failed to parse URL."; + return result; + } + + const BOOL opened = [[NSWorkspace sharedWorkspace] openURL:target]; + if (!opened) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = "NSWorkspace could not open the URL."; + return result; + } + + UrlOpenResult result; + result.success = true; + result.error_code = UrlOpenErrorCode::kNone; + return result; + } + } +}; + +} // namespace + +UrlOpener::UrlOpener() : pimpl_(std::make_unique()) {} + +UrlOpener::~UrlOpener() = default; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm new file mode 100644 index 0000000..ef8fed7 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm @@ -0,0 +1,544 @@ +#include +#include "../../foundation/id_allocator.h" +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" +#include "coordinate_utils_macos.h" + +// Import Cocoa headers +#import +#import + +// Key for associated objects (used by both window_macos.mm and window_manager_macos.mm) +const void* kWindowIdKey = &kWindowIdKey; + +namespace nativeapi { + +// Private implementation class +class Window::Impl { + public: + Impl(WindowId id, NSWindow* window) + : id_(id), + ns_window_(window), + title_bar_style_(TitleBarStyle::Normal), + visual_effect_(VisualEffect::None), + visual_effect_view_(nil) {} + WindowId id_; + NSWindow* ns_window_; + TitleBarStyle title_bar_style_; + VisualEffect visual_effect_; + NSVisualEffectView* visual_effect_view_; +}; + +Window::Window() : Window(nullptr) {} + +Window::Window(void* native_window) { + NSWindow* ns_window = nullptr; + WindowId id; + + if (native_window == nullptr) { + // Create new platform object + id = IdAllocator::Allocate(); + ns_window = [[NSWindow alloc] init]; + ns_window.styleMask = NSWindowStyleMaskResizable | NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + // Store the ID as associated object + objc_setAssociatedObject(ns_window, kWindowIdKey, [NSNumber numberWithUnsignedLongLong:id], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + // Wrap existing platform object - check if it already has an ID + ns_window = (__bridge NSWindow*)native_window; + NSNumber* existingId = objc_getAssociatedObject(ns_window, kWindowIdKey); + if (existingId) { + // Use existing ID + id = [existingId unsignedLongLongValue]; + } else { + // Allocate new ID and store it + id = IdAllocator::Allocate(); + objc_setAssociatedObject(ns_window, kWindowIdKey, [NSNumber numberWithUnsignedLongLong:id], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + } + + // All initialization logic in one place + pimpl_ = std::make_unique(id, ns_window); +} + +Window::~Window() {} + +void Window::Focus() { + [pimpl_->ns_window_ makeKeyAndOrderFront:nil]; +} + +void Window::Blur() { + [pimpl_->ns_window_ orderBack:nil]; +} + +bool Window::IsFocused() const { + return [pimpl_->ns_window_ isKeyWindow]; +} + +void Window::Show() { + [pimpl_->ns_window_ setIsVisible:YES]; + // Panels receive key focus when shown but should not activate the app. + if (![pimpl_->ns_window_ isKindOfClass:[NSPanel class]]) { + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + } + [pimpl_->ns_window_ makeKeyAndOrderFront:nil]; +} + +void Window::ShowInactive() { + [pimpl_->ns_window_ setIsVisible:YES]; + [pimpl_->ns_window_ orderFrontRegardless]; +} + +void Window::Hide() { + [pimpl_->ns_window_ setIsVisible:NO]; + [pimpl_->ns_window_ orderOut:nil]; +} + +bool Window::IsVisible() const { + return [pimpl_->ns_window_ isVisible]; +} + +void Window::Maximize() { + if (!IsMaximized()) { + [pimpl_->ns_window_ zoom:nil]; + } +} + +void Window::Unmaximize() { + if (IsMaximized()) { + [pimpl_->ns_window_ zoom:nil]; + } +} + +bool Window::IsMaximized() const { + return [pimpl_->ns_window_ isZoomed]; +} + +void Window::Minimize() { + if (!IsMinimized()) { + [pimpl_->ns_window_ miniaturize:nil]; + } +} + +void Window::Restore() { + if (IsMinimized()) { + [pimpl_->ns_window_ deminiaturize:nil]; + } +} + +bool Window::IsMinimized() const { + return [pimpl_->ns_window_ isMiniaturized]; +} + +void Window::SetFullScreen(bool is_full_screen) { + if (is_full_screen) { + if (!IsFullScreen()) { + [pimpl_->ns_window_ toggleFullScreen:nil]; + } + } else { + if (IsFullScreen()) { + [pimpl_->ns_window_ toggleFullScreen:nil]; + } + } +} + +bool Window::IsFullScreen() const { + return [pimpl_->ns_window_ styleMask] & NSWindowStyleMaskFullScreen; +} + +//// void Window::SetBackgroundColor(Color color); +//// Color Window::GetBackgroundColor() const; + +void Window::SetBounds(Rectangle bounds) { + // Convert from topLeft coordinate system to bottom-left (macOS default) + NSRect topLeftRect = NSMakeRect(bounds.x, bounds.y, bounds.width, bounds.height); + NSRect nsRect = NSRectExt::bottomLeft(topLeftRect); + [pimpl_->ns_window_ setFrame:nsRect display:YES]; +} + +Rectangle Window::GetBounds() const { + NSRect frame = [pimpl_->ns_window_ frame]; + // Convert from bottom-left (macOS default) to top-left coordinate system + CGPoint topLeft = NSRectExt::topLeft(frame); + Rectangle bounds = {topLeft.x, topLeft.y, static_cast(frame.size.width), + static_cast(frame.size.height)}; + return bounds; +} + +void Window::SetSize(Size size, bool animate) { + NSRect frame = [pimpl_->ns_window_ frame]; + frame.origin.y += (frame.size.height - size.height); + frame.size.width = size.width; + frame.size.height = size.height; + if (animate) { + [[pimpl_->ns_window_ animator] setFrame:frame display:YES animate:YES]; + } else { + [pimpl_->ns_window_ setFrame:frame display:YES]; + } +} + +Size Window::GetSize() const { + NSRect frame = [pimpl_->ns_window_ frame]; + Size size = {static_cast(frame.size.width), static_cast(frame.size.height)}; + return size; +} + +void Window::SetContentSize(Size size) { + [pimpl_->ns_window_ setContentSize:NSMakeSize(size.width, size.height)]; +} + +Size Window::GetContentSize() const { + NSRect frame = [pimpl_->ns_window_ contentRectForFrameRect:[pimpl_->ns_window_ frame]]; + Size size = {static_cast(frame.size.width), static_cast(frame.size.height)}; + return size; +} + +void Window::SetContentBounds(Rectangle bounds) { + // Convert from topLeft coordinate system to bottom-left (macOS default) + NSRect topLeftRect = NSMakeRect(bounds.x, bounds.y, bounds.width, bounds.height); + NSRect contentRect = NSRectExt::bottomLeft(topLeftRect); + + // Set the content view frame + NSRect frameRect = [pimpl_->ns_window_ frameRectForContentRect:contentRect]; + [pimpl_->ns_window_ setFrame:frameRect display:YES]; +} + +Rectangle Window::GetContentBounds() const { + NSRect contentRect = [pimpl_->ns_window_ contentRectForFrameRect:[pimpl_->ns_window_ frame]]; + // Convert from bottom-left (macOS default) to top-left coordinate system + CGPoint topLeft = NSRectExt::topLeft(contentRect); + Rectangle bounds = {topLeft.x, topLeft.y, static_cast(contentRect.size.width), + static_cast(contentRect.size.height)}; + return bounds; +} + +void Window::SetMinimumSize(Size size) { + [pimpl_->ns_window_ setMinSize:NSMakeSize(size.width, size.height)]; +} + +Size Window::GetMinimumSize() const { + NSSize size = [pimpl_->ns_window_ minSize]; + return Size{static_cast(size.width), static_cast(size.height)}; +} + +void Window::SetMaximumSize(Size size) { + [pimpl_->ns_window_ setMaxSize:NSMakeSize(size.width, size.height)]; +} + +Size Window::GetMaximumSize() const { + NSSize size = [pimpl_->ns_window_ maxSize]; + return Size{static_cast(size.width), static_cast(size.height)}; +} + +void Window::SetResizable(bool is_resizable) { + NSUInteger style_mask = [pimpl_->ns_window_ styleMask]; + if (is_resizable) { + style_mask |= NSWindowStyleMaskResizable; + } else { + style_mask &= ~NSWindowStyleMaskResizable; + } + [pimpl_->ns_window_ setStyleMask:style_mask]; +} + +bool Window::IsResizable() const { + return [pimpl_->ns_window_ styleMask] & NSWindowStyleMaskResizable; +} + +void Window::SetMovable(bool is_movable) { + [pimpl_->ns_window_ setMovable:is_movable]; +} + +bool Window::IsMovable() const { + return [pimpl_->ns_window_ isMovable]; +} + +void Window::SetMinimizable(bool is_minimizable) { + NSUInteger style_mask = [pimpl_->ns_window_ styleMask]; + if (is_minimizable) { + style_mask |= NSWindowStyleMaskMiniaturizable; + } else { + style_mask &= ~NSWindowStyleMaskMiniaturizable; + } + [pimpl_->ns_window_ setStyleMask:style_mask]; +} + +bool Window::IsMinimizable() const { + return [pimpl_->ns_window_ styleMask] & NSWindowStyleMaskMiniaturizable; +} + +void Window::SetMaximizable(bool is_maximizable) { + NSUInteger style_mask = [pimpl_->ns_window_ styleMask]; + if (is_maximizable) { + style_mask |= NSWindowStyleMaskResizable; + } else { + style_mask &= ~NSWindowStyleMaskResizable; + } + [pimpl_->ns_window_ setStyleMask:style_mask]; +} + +bool Window::IsMaximizable() const { + return [pimpl_->ns_window_ styleMask] & NSWindowStyleMaskResizable; +} + +void Window::SetFullScreenable(bool is_full_screenable) { + // TODO: Implement this +} + +bool Window::IsFullScreenable() const { + return [pimpl_->ns_window_ styleMask] & NSWindowStyleMaskFullScreen; +} + +void Window::SetClosable(bool is_closable) { + NSUInteger style_mask = [pimpl_->ns_window_ styleMask]; + if (is_closable) { + style_mask |= NSWindowStyleMaskClosable; + } else { + style_mask &= ~NSWindowStyleMaskClosable; + } + [pimpl_->ns_window_ setStyleMask:style_mask]; +} + +bool Window::IsClosable() const { + return [pimpl_->ns_window_ styleMask] & NSWindowStyleMaskClosable; +} + +void Window::SetWindowControlButtonsVisible(bool is_visible) { + NSButton* closeButton = [pimpl_->ns_window_ standardWindowButton:NSWindowCloseButton]; + NSButton* miniaturizeButton = [pimpl_->ns_window_ standardWindowButton:NSWindowMiniaturizeButton]; + NSButton* zoomButton = [pimpl_->ns_window_ standardWindowButton:NSWindowZoomButton]; + + if (closeButton) { + [closeButton setHidden:!is_visible]; + } + if (miniaturizeButton) { + [miniaturizeButton setHidden:!is_visible]; + } + if (zoomButton) { + [zoomButton setHidden:!is_visible]; + } +} + +bool Window::IsWindowControlButtonsVisible() const { + NSButton* closeButton = [pimpl_->ns_window_ standardWindowButton:NSWindowCloseButton]; + if (closeButton) { + return ![closeButton isHidden]; + } + return true; // Default to visible if button not found +} + +void Window::SetAlwaysOnTop(bool is_always_on_top) { + [pimpl_->ns_window_ setLevel:is_always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel]; +} + +bool Window::IsAlwaysOnTop() const { + return [pimpl_->ns_window_ level] == NSFloatingWindowLevel; +} + +void Window::SetPosition(Point point) { + // Convert from topLeft coordinate system to bottom-left (macOS default) + // We need the window height to correctly convert the top-left position + NSRect frame = [pimpl_->ns_window_ frame]; + CGPoint topLeftPoint = {point.x, point.y}; + NSPoint bottomLeft = NSPointExt::bottomLeftForWindow(topLeftPoint, frame.size.height); + [pimpl_->ns_window_ setFrameOrigin:bottomLeft]; +} + +Point Window::GetPosition() const { + NSRect frame = [pimpl_->ns_window_ frame]; + // Convert from bottom-left (macOS default) to top-left coordinate system + CGPoint topLeft = NSRectExt::topLeft(frame); + Point point = {topLeft.x, topLeft.y}; + return point; +} + +void Window::Center() { + // Use NSWindow's center method which automatically centers on the main screen + [pimpl_->ns_window_ center]; +} + +void Window::SetTitle(std::string title) { + [pimpl_->ns_window_ setTitle:[NSString stringWithUTF8String:title.c_str()]]; +} + +std::string Window::GetTitle() const { + NSString* title = [pimpl_->ns_window_ title]; + return title ? std::string([title UTF8String]) : std::string(); +} + +void Window::SetTitleBarStyle(TitleBarStyle style) { + pimpl_->title_bar_style_ = style; + + if (style == TitleBarStyle::Hidden) { + // Hide title bar - make it transparent and full size content view + pimpl_->ns_window_.titleVisibility = NSWindowTitleHidden; + pimpl_->ns_window_.titlebarAppearsTransparent = YES; + pimpl_->ns_window_.styleMask |= NSWindowStyleMaskFullSizeContentView; + } else { + // Show title bar - restore normal appearance + pimpl_->ns_window_.titleVisibility = NSWindowTitleVisible; + pimpl_->ns_window_.titlebarAppearsTransparent = NO; + pimpl_->ns_window_.styleMask &= ~NSWindowStyleMaskFullSizeContentView; + } + + // Ensure window remains opaque and has shadow + pimpl_->ns_window_.opaque = NO; + pimpl_->ns_window_.hasShadow = YES; + + // Show window buttons + NSView* titleBarView = + [[pimpl_->ns_window_ standardWindowButton:NSWindowCloseButton] superview].superview; + if (titleBarView) { + titleBarView.hidden = NO; + } + + [pimpl_->ns_window_ standardWindowButton:NSWindowCloseButton].hidden = NO; + [pimpl_->ns_window_ standardWindowButton:NSWindowMiniaturizeButton].hidden = NO; + [pimpl_->ns_window_ standardWindowButton:NSWindowZoomButton].hidden = NO; +} + +TitleBarStyle Window::GetTitleBarStyle() const { + return pimpl_->title_bar_style_; +} + +void Window::SetHasShadow(bool has_shadow) { + [pimpl_->ns_window_ setHasShadow:has_shadow]; + [pimpl_->ns_window_ invalidateShadow]; +} + +bool Window::HasShadow() const { + return [pimpl_->ns_window_ hasShadow]; +} + +void Window::SetOpacity(float opacity) { + [pimpl_->ns_window_ setAlphaValue:opacity]; +} + +float Window::GetOpacity() const { + return [pimpl_->ns_window_ alphaValue]; +} + +void Window::SetVisualEffect(VisualEffect effect) { + if (pimpl_->visual_effect_ == effect) + return; + + pimpl_->visual_effect_ = effect; + NSWindow* window = pimpl_->ns_window_; + + if (effect == VisualEffect::None) { + if (pimpl_->visual_effect_view_) { + [pimpl_->visual_effect_view_ removeFromSuperview]; + pimpl_->visual_effect_view_ = nil; + } + [window setOpaque:YES]; + [window setBackgroundColor:[NSColor windowBackgroundColor]]; + return; + } + + if (!pimpl_->visual_effect_view_) { + NSView* contentView = [window contentView]; + pimpl_->visual_effect_view_ = [[NSVisualEffectView alloc] initWithFrame:[contentView bounds]]; + [pimpl_->visual_effect_view_ setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [pimpl_->visual_effect_view_ setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [contentView addSubview:pimpl_->visual_effect_view_ positioned:NSWindowBelow relativeTo:nil]; + } + + [window setOpaque:NO]; + [window setBackgroundColor:[NSColor clearColor]]; + + switch (effect) { + case VisualEffect::Blur: + [pimpl_->visual_effect_view_ setMaterial:NSVisualEffectMaterialSidebar]; + break; + case VisualEffect::Acrylic: + [pimpl_->visual_effect_view_ setMaterial:NSVisualEffectMaterialUnderWindowBackground]; + break; + case VisualEffect::Mica: + [pimpl_->visual_effect_view_ setMaterial:NSVisualEffectMaterialWindowBackground]; + break; + default: + break; + } + + [pimpl_->visual_effect_view_ setState:NSVisualEffectStateActive]; +} + +VisualEffect Window::GetVisualEffect() const { + return pimpl_->visual_effect_; +} + +void Window::SetBackgroundColor(const Color& color) { + NSColor* nsColor = [NSColor colorWithRed:color.r / 255.0 + green:color.g / 255.0 + blue:color.b / 255.0 + alpha:color.a / 255.0]; + [pimpl_->ns_window_ setBackgroundColor:nsColor]; +} + +Color Window::GetBackgroundColor() const { + NSColor* nsColor = [pimpl_->ns_window_ backgroundColor]; + + // Convert NSColor to RGB color space if needed + NSColor* rgbColor = [nsColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (!rgbColor) { + // Fallback if conversion fails + return Color::White; + } + + CGFloat r, g, b, a; + [rgbColor getRed:&r green:&g blue:&b alpha:&a]; + + return Color::FromRGBA( + static_cast(r * 255), + static_cast(g * 255), + static_cast(b * 255), + static_cast(a * 255) + ); +} + +void Window::SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces) { + [pimpl_->ns_window_ setCollectionBehavior:is_visible_on_all_workspaces + ? NSWindowCollectionBehaviorCanJoinAllSpaces + : NSWindowCollectionBehaviorDefault]; +} + +bool Window::IsVisibleOnAllWorkspaces() const { + return [pimpl_->ns_window_ collectionBehavior] & NSWindowCollectionBehaviorCanJoinAllSpaces; +} + +void Window::SetIgnoreMouseEvents(bool is_ignore_mouse_events) { + [pimpl_->ns_window_ setIgnoresMouseEvents:is_ignore_mouse_events]; +} + +bool Window::IsIgnoreMouseEvents() const { + return [pimpl_->ns_window_ ignoresMouseEvents]; +} + +void Window::SetFocusable(bool is_focusable) { + // TODO: Implement this +} + +bool Window::IsFocusable() const { + return [pimpl_->ns_window_ canBecomeKeyWindow]; +} + +void Window::StartDragging() { + NSWindow* window = pimpl_->ns_window_; + if (window.currentEvent) { + [window performWindowDragWithEvent:window.currentEvent]; + } +} + +void Window::StartResizing() {} + +WindowId Window::GetId() const { + return pimpl_->id_; +} + +void* Window::GetNativeObjectInternal() const { + return (__bridge void*)pimpl_->ns_window_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/window_manager_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/window_manager_macos.mm new file mode 100644 index 0000000..f28471b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/window_manager_macos.mm @@ -0,0 +1,467 @@ +#import +#import +#include +#include +#include + +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" + +// Forward declaration for the delegate +@class NativeAPIWindowManagerDelegate; + +// External declaration of kWindowIdKey (defined in window_macos.mm) +extern const void* kWindowIdKey; + +namespace nativeapi { + +// Private implementation to hide Objective-C details +class WindowManager::Impl { + public: + Impl(WindowManager* manager); + ~Impl(); + void StartEventListening(); + void StopEventListening(); + void OnWindowEvent(NSWindow* window, const std::string& event_type); + + private: + WindowManager* manager_; + NativeAPIWindowManagerDelegate* delegate_; + + // Optional pre-show/hide/close hooks + std::optional will_show_hook_; + std::optional will_hide_hook_; + std::optional will_close_hook_; + + friend class WindowManager; +}; + +} // namespace nativeapi + +// MARK: - NSWindow Swizzling + +// Swizzled implementations call into WindowManager hooks, then forward to original implementations +@interface NSWindow (NativeAPISwizzle) +- (void)na_swizzled_makeKeyAndOrderFront:(id)sender; +- (void)na_swizzled_orderOut:(id)sender; +- (void)na_swizzled_performClose:(id)sender; +@end + +@implementation NSWindow (NativeAPISwizzle) + +- (void)na_swizzled_makeKeyAndOrderFront:(id)sender { + // Resolve window id and handle hook if present + if (nativeapi::WindowManager::GetInstance().HasWillShowHook()) { + auto windows = nativeapi::WindowManager::GetInstance().GetAll(); + for (const auto& window : windows) { + if (window->GetNativeObject() == (__bridge void*)self) { + nativeapi::WindowManager::GetInstance().HandleWillShow(window->GetId()); + // Hook handles all logic; never call original here + return; + } + } + } + // No window found in registry, call original implementation (swapped) + [self na_swizzled_makeKeyAndOrderFront:sender]; +} + +- (void)na_swizzled_orderOut:(id)sender { + // Resolve window id and handle hook if present + if (nativeapi::WindowManager::GetInstance().HasWillHideHook()) { + auto windows = nativeapi::WindowManager::GetInstance().GetAll(); + for (const auto& window : windows) { + if (window->GetNativeObject() == (__bridge void*)self) { + nativeapi::WindowManager::GetInstance().HandleWillHide(window->GetId()); + return; + } + } + } + // No window found in registry, call original implementation (swapped) + [self na_swizzled_orderOut:sender]; +} + +- (void)na_swizzled_performClose:(id)sender { + // Resolve window id and handle hook if present + if (nativeapi::WindowManager::GetInstance().HasWillCloseHook()) { + auto windows = nativeapi::WindowManager::GetInstance().GetAll(); + for (const auto& window : windows) { + if (window->GetNativeObject() == (__bridge void*)self) { + nativeapi::WindowManager::GetInstance().HandleWillClose(window->GetId()); + // Hook handles all logic; never call original here + return; + } + } + } + // No window found in registry, call original implementation (swapped) + [self na_swizzled_performClose:sender]; +} + +@end + +static void NativeAPIInstallNSWindowWillShowSwizzleOnce() { + static dispatch_once_t onceTokenShow; + dispatch_once(&onceTokenShow, ^{ + Class cls = [NSWindow class]; + SEL originalSel = @selector(makeKeyAndOrderFront:); + SEL swizzledSel = @selector(na_swizzled_makeKeyAndOrderFront:); + Method original = class_getInstanceMethod(cls, originalSel); + Method swizzled = class_getInstanceMethod(cls, swizzledSel); + if (original && swizzled) { + method_exchangeImplementations(original, swizzled); + } + }); +} + +static void NativeAPIInstallNSWindowWillHideSwizzleOnce() { + static dispatch_once_t onceTokenHide; + dispatch_once(&onceTokenHide, ^{ + Class cls = [NSWindow class]; + SEL originalSel = @selector(orderOut:); + SEL swizzledSel = @selector(na_swizzled_orderOut:); + Method original = class_getInstanceMethod(cls, originalSel); + Method swizzled = class_getInstanceMethod(cls, swizzledSel); + if (original && swizzled) { + method_exchangeImplementations(original, swizzled); + } + }); +} + +static void NativeAPIInstallNSWindowWillCloseSwizzleOnce() { + static dispatch_once_t onceTokenClose; + dispatch_once(&onceTokenClose, ^{ + Class cls = [NSWindow class]; + SEL originalSel = @selector(performClose:); + SEL swizzledSel = @selector(na_swizzled_performClose:); + Method original = class_getInstanceMethod(cls, originalSel); + Method swizzled = class_getInstanceMethod(cls, swizzledSel); + if (original && swizzled) { + method_exchangeImplementations(original, swizzled); + } + }); +} + +// Objective-C delegate class to handle NSWindow notifications +@interface NativeAPIWindowManagerDelegate : NSObject +@property(nonatomic, assign) void* impl; // Use void* instead of private class +- (instancetype)initWithImpl:(void*)impl; +@end + +@implementation NativeAPIWindowManagerDelegate + +- (instancetype)initWithImpl:(void*)impl { + if (self = [super init]) { + _impl = impl; + } + return self; +} + +- (void)windowDidBecomeKey:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "focused"); + } +} + +- (void)windowDidResignKey:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "blurred"); + } +} + +- (void)windowDidMiniaturize:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "minimized"); + } +} + +- (void)windowDidDeminiaturize:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "restored"); + } +} + +- (void)windowDidResize:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "resized"); + } +} + +- (void)windowDidMove:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "moved"); + } +} + +- (void)windowWillClose:(NSNotification*)notification { + // NSWindow* window = [notification object]; + if (_impl) { + // static_cast(_impl)->OnWindowEvent(window, "closing"); + } +} + +@end + +namespace nativeapi { + +WindowManager::Impl::Impl(WindowManager* manager) : manager_(manager), delegate_(nullptr) {} + +WindowManager::Impl::~Impl() { + StopEventListening(); +} + +void WindowManager::Impl::StartEventListening() { + if (!delegate_) { + delegate_ = [[NativeAPIWindowManagerDelegate alloc] initWithImpl:this]; + + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:delegate_ + selector:@selector(windowDidBecomeKey:) + name:NSWindowDidBecomeKeyNotification + object:nil]; + [center addObserver:delegate_ + selector:@selector(windowDidResignKey:) + name:NSWindowDidResignKeyNotification + object:nil]; + [center addObserver:delegate_ + selector:@selector(windowDidMiniaturize:) + name:NSWindowDidMiniaturizeNotification + object:nil]; + [center addObserver:delegate_ + selector:@selector(windowDidDeminiaturize:) + name:NSWindowDidDeminiaturizeNotification + object:nil]; + [center addObserver:delegate_ + selector:@selector(windowDidResize:) + name:NSWindowDidResizeNotification + object:nil]; + [center addObserver:delegate_ + selector:@selector(windowDidMove:) + name:NSWindowDidMoveNotification + object:nil]; + [center addObserver:delegate_ + selector:@selector(windowWillClose:) + name:NSWindowWillCloseNotification + object:nil]; + } +} + +void WindowManager::Impl::StopEventListening() { + if (delegate_) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:delegate_]; + delegate_ = nil; + } +} + +void WindowManager::Impl::OnWindowEvent(NSWindow* window, const std::string& event_type) { + WindowId window_id = [window windowNumber]; + + if (event_type == "focused") { + WindowFocusedEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "blurred") { + WindowBlurredEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "minimized") { + WindowMinimizedEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "restored") { + WindowRestoredEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "resized") { + NSRect frame = [window frame]; + Size new_size = {frame.size.width, frame.size.height}; + WindowResizedEvent event(window_id, new_size); + manager_->DispatchWindowEvent(event); + } else if (event_type == "moved") { + NSRect frame = [window frame]; + Point new_position = {frame.origin.x, frame.origin.y}; + WindowMovedEvent event(window_id, new_position); + manager_->DispatchWindowEvent(event); + } else if (event_type == "closing") { + // Window closing event - no longer emitted + } +} + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + StartEventListening(); +} + +WindowManager::~WindowManager() { + StopEventListening(); +} + +std::shared_ptr WindowManager::Get(WindowId id) { + // First check if it's already in the registry + auto window = WindowRegistry::GetInstance().Get(id); + if (window) { + return window; + } + + // If not found, ensure all NSWindows are registered and try again + GetAll(); + return WindowRegistry::GetInstance().Get(id); +} + +std::vector> WindowManager::GetAll() { + NSArray* ns_windows = [[NSApplication sharedApplication] windows]; + + // First, ensure all NSWindows are registered + for (NSWindow* ns_window in ns_windows) { + // Create or get Window wrapper - this will handle ID assignment via associated object + auto window = std::make_shared((__bridge void*)ns_window); + WindowId window_id = window->GetId(); + + // Add to registry if not already present + if (!WindowRegistry::GetInstance().Get(window_id)) { + WindowRegistry::GetInstance().Add(window_id, window); + } + } + + // Then return all windows from registry (which now includes all NSWindows) + return WindowRegistry::GetInstance().GetAll(); +} + +std::shared_ptr WindowManager::GetCurrent() { + NSApplication* app = [NSApplication sharedApplication]; + NSArray* ns_windows = [[NSApplication sharedApplication] windows]; + NSWindow* ns_window = [app mainWindow]; + if (ns_window == nil && [ns_windows count] > 0) { + ns_window = [ns_windows objectAtIndex:0]; + } + if (ns_window != nil) { + // First, try to get the window ID from the associated object + NSNumber* existingIdNumber = objc_getAssociatedObject(ns_window, kWindowIdKey); + if (existingIdNumber) { + WindowId window_id = [existingIdNumber unsignedLongLongValue]; + + // Try to get the existing Window from registry + auto existing_window = WindowRegistry::GetInstance().Get(window_id); + if (existing_window) { + return existing_window; + } + } + + // If not found in registry, create a new Window wrapper + auto window = std::make_shared((__bridge void*)ns_window); + WindowId window_id = window->GetId(); + + // Add to registry (temporary solution) + WindowRegistry::GetInstance().Add(window_id, window); + return window; + } + return nullptr; +} + +void WindowManager::SetWillShowHook(std::optional hook) { + pimpl_->will_show_hook_ = std::move(hook); + if (pimpl_->will_show_hook_) { + NativeAPIInstallNSWindowWillShowSwizzleOnce(); + } +} + +void WindowManager::SetWillHideHook(std::optional hook) { + pimpl_->will_hide_hook_ = std::move(hook); + if (pimpl_->will_hide_hook_) { + NativeAPIInstallNSWindowWillHideSwizzleOnce(); + } +} + +void WindowManager::SetWillCloseHook(std::optional hook) { + pimpl_->will_close_hook_ = std::move(hook); + if (pimpl_->will_close_hook_) { + NativeAPIInstallNSWindowWillCloseSwizzleOnce(); + } +} + +bool WindowManager::HasWillShowHook() const { + return pimpl_->will_show_hook_.has_value(); +} + +bool WindowManager::HasWillHideHook() const { + return pimpl_->will_hide_hook_.has_value(); +} + +bool WindowManager::HasWillCloseHook() const { + return pimpl_->will_close_hook_.has_value(); +} + +void WindowManager::HandleWillShow(WindowId id) { + if (pimpl_->will_show_hook_) { + (*pimpl_->will_show_hook_)(id); + } +} + +void WindowManager::HandleWillHide(WindowId id) { + if (pimpl_->will_hide_hook_) { + (*pimpl_->will_hide_hook_)(id); + } +} + +void WindowManager::HandleWillClose(WindowId id) { + if (pimpl_->will_close_hook_) { + (*pimpl_->will_close_hook_)(id); + } +} + +bool WindowManager::CallOriginalShow(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + NSWindow* ns_window = (__bridge NSWindow*)native; + [ns_window na_swizzled_makeKeyAndOrderFront:nil]; + return true; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + NSWindow* ns_window = (__bridge NSWindow*)native; + [ns_window na_swizzled_orderOut:nil]; + return true; +} + +bool WindowManager::CallOriginalClose(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + NSWindow* ns_window = (__bridge NSWindow*)native; + [ns_window na_swizzled_performClose:nil]; + return true; +} + +void WindowManager::StartEventListening() { + pimpl_->StartEventListening(); +} + +void WindowManager::StopEventListening() { + pimpl_->StopEventListening(); +} + +void WindowManager::DispatchWindowEvent(const WindowEvent& event) { + Emit(event); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/accessibility_manager_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/accessibility_manager_ohos.cpp new file mode 100644 index 0000000..62bc139 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/accessibility_manager_ohos.cpp @@ -0,0 +1,23 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include "../../accessibility_manager.h" + +#ifdef __OHOS__ +#define HILOG_WARN(...) HILOG_WARN(LOG_CORE, LOG_TAG, __VA_ARGS__) +#else +#define HILOG_WARN(...) std::cerr << "[WARN] " << __VA_ARGS__ << std::endl +#endif + +namespace nativeapi { + +void AccessibilityManager::Enable() { + enabled_ = true; +} + +bool AccessibilityManager::IsEnabled() { + return enabled_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/application_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/application_ohos.cpp new file mode 100644 index 0000000..b507813 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/application_ohos.cpp @@ -0,0 +1,71 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include "../../application.h" +#include "../../window_manager.h" + +// Temporarily disable logging to avoid macro conflicts +#define HILOG_WARN(...) ((void)0) + +namespace nativeapi { + +class Application::Impl { + public: + Impl() {} +}; + +Application::Application() : pimpl_(std::make_unique()) {} +Application::~Application() {} + +int Application::Run() { + HILOG_WARN("Application::Run not applicable on OpenHarmony (handled by Ability lifecycle)"); + return 0; +} + +int Application::Run(std::shared_ptr window) { + HILOG_WARN("Application::Run with window not applicable on OpenHarmony"); + return 0; +} + +void Application::Quit(int exit_code) { + HILOG_WARN("Application::Quit requests Ability terminate"); +} + +bool Application::IsRunning() const { + return true; +} + +bool Application::IsSingleInstance() const { + return false; +} + +bool Application::SetIcon(const std::string& icon_path) { + HILOG_WARN("Application::SetIcon not implemented on OpenHarmony"); + return false; +} + +bool Application::SetDockIconVisible(bool visible) { + HILOG_WARN("Application::SetDockIconVisible not applicable on OpenHarmony"); + return false; +} + +bool Application::SetMenuBar(std::shared_ptr menu) { + HILOG_WARN("Application::SetMenuBar not implemented on OpenHarmony"); + return false; +} + +std::shared_ptr Application::GetPrimaryWindow() const { + return nullptr; +} + +void Application::SetPrimaryWindow(std::shared_ptr window) { + HILOG_WARN("Application::SetPrimaryWindow not implemented on OpenHarmony"); +} + +std::vector> Application::GetAllWindows() const { + auto& window_manager = WindowManager::GetInstance(); + return window_manager.GetAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/dispatcher_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/dispatcher_ohos.cpp new file mode 100644 index 0000000..4bcfcad --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/dispatcher_ohos.cpp @@ -0,0 +1,34 @@ +#include "../../foundation/dispatcher_platform.h" +#include "../../foundation/dispatcher_common.h" + +namespace nativeapi { +namespace dispatcher_platform { + +bool PlatformIsMainThread() { + return dispatcher_internal::IsMainThreadByCapturedId(); +} + +void PlatformSetMainThread() { + dispatcher_internal::CaptureCallerAsMainThread(); +} + +bool PlatformIsMainThreadDispatchSupported() { + return false; +} + +bool PlatformRunOnMainThread(std::function fn) { + // TODO(ohos): implement via the ArkUI event handler / OH_Napi thread-safe + // function, whichever the surrounding app model provides. + // + // Returning false rather than dropping silently — see the Android note. + (void)fn; + return false; +} + +bool PlatformRunMainThreadLoopFor(int timeout_ms) { + (void)timeout_ms; + return false; +} + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/display_manager_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/display_manager_ohos.cpp new file mode 100644 index 0000000..7076d48 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/display_manager_ohos.cpp @@ -0,0 +1,25 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include "../../display_manager.h" + +// Temporarily disable logging to avoid macro conflicts +#define HILOG_WARN(...) ((void)0) + +namespace nativeapi { + +DisplayManager::DisplayManager() {} + +DisplayManager::~DisplayManager() {} + +std::vector DisplayManager::EnumerateNativeDisplays() { + // Stub: a single default display. + return {{"primary", nullptr, true}}; +} + +Point DisplayManager::GetCursorPosition() { + return Point{0, 0}; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/display_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/display_ohos.cpp new file mode 100644 index 0000000..a1149be --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/display_ohos.cpp @@ -0,0 +1,79 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include "../../display.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class Display::Impl { + public: + Impl() = default; + Impl(void* display) : native_display_(display) {} + + const DisplayId id_ = IdAllocator::Allocate(); + void* native_display_ = nullptr; +}; + +Display::Display(void* display) : pimpl_(std::make_unique(display)) {} + +Display::~Display() = default; + +void* Display::GetNativeObjectInternal() const { + return pimpl_->native_display_; +} + +DisplayId Display::GetId() const { + return pimpl_->id_; +} + +std::string Display::GetName() const { + return "Primary Display"; +} + +Point Display::GetPosition() const { + return {0.0, 0.0}; +} + +Size Display::GetSize() const { + // Default display size for OpenHarmony devices + return {360.0, 780.0}; +} + +Rectangle Display::GetWorkArea() const { + // Default work area matches display size + Size size = GetSize(); + return {0.0, 0.0, size.width, size.height}; +} + +double Display::GetScaleFactor() const { + return 1.0; +} + +bool Display::IsPrimary() const { + return true; +} + +DisplayOrientation Display::GetOrientation() const { + return DisplayOrientation::kPortrait; +} + +int Display::GetRefreshRate() const { + return 60; +} + +int Display::GetBitDepth() const { + return 32; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/image_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/image_ohos.cpp new file mode 100644 index 0000000..49585ac --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/image_ohos.cpp @@ -0,0 +1,69 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include "../../image.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class Image::Impl { + public: + Impl() = default; + + void* native_image_ = nullptr; + std::string source_; + Size size_ = {0, 0}; + std::string format_ = "Unknown"; +}; + +Image::Image() : pimpl_(std::make_unique()) {} + +Image::~Image() = default; + +Image::Image(const Image& other) : pimpl_(std::make_unique(*other.pimpl_)) {} + +Image::Image(Image&& other) noexcept : pimpl_(std::move(other.pimpl_)) {} + +std::shared_ptr Image::FromFile(const std::string& file_path) { + // Return nullptr - not implemented on OpenHarmony yet + return nullptr; +} + +std::shared_ptr Image::FromBase64(const std::string& base64_data) { + // Return nullptr - not implemented on OpenHarmony yet + return nullptr; +} + +Size Image::GetSize() const { + return pimpl_->size_; +} + +std::string Image::GetFormat() const { + return pimpl_->format_; +} + +std::string Image::ToBase64() const { + // Return empty string - not implemented on OpenHarmony yet + return ""; +} + +bool Image::SaveToFile(const std::string& file_path) const { + // Return false - not implemented on OpenHarmony yet + return false; +} + +void* Image::GetNativeObjectInternal() const { + return pimpl_->native_image_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/keyboard_monitor_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/keyboard_monitor_ohos.cpp new file mode 100644 index 0000000..2c32da5 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/keyboard_monitor_ohos.cpp @@ -0,0 +1,46 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include "../../keyboard_monitor.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class KeyboardMonitor::Impl { + public: + Impl(KeyboardMonitor* monitor) : monitor_(monitor) {} + + KeyboardMonitor* monitor_; +}; + +KeyboardMonitor::KeyboardMonitor() : impl_(std::make_unique(this)) {} + +KeyboardMonitor::~KeyboardMonitor() { + Stop(); +} + +void KeyboardMonitor::Start() { + // Not implemented on OpenHarmony yet +} + +void KeyboardMonitor::Stop() { + // Not implemented on OpenHarmony yet +} + +bool KeyboardMonitor::IsMonitoring() const { + return false; +} + +EventEmitter& KeyboardMonitor::GetInternalEventEmitter() { + return *this; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/launch_at_login_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/launch_at_login_ohos.cpp new file mode 100644 index 0000000..12be1b3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/launch_at_login_ohos.cpp @@ -0,0 +1,105 @@ +#include "../../launch_at_login.h" + +namespace nativeapi { + +/** + * OHOS stub implementation for LaunchAtLogin. + * + * Auto-start at user login/session is not supported on OHOS in this library. + * All operations that would enable/disable or configure launch-at-login return false. + * Getters return the locally stored values (typically empty), while setters + * return false and do not modify state. + */ +class LaunchAtLogin::Impl { + public: + // Unsupported platform semantics + static bool IsSupported() { return false; } + + Impl() = default; + + explicit Impl(const std::string& id) : id_(id) {} + + Impl(const std::string& id, const std::string& display_name) + : id_(id), display_name_(display_name) {} + + ~Impl() = default; + + // Getters return whatever is locally available (likely empty) + std::string GetId() const { return id_; } + std::string GetDisplayName() const { return display_name_; } + + // No-op setter; returns false to indicate unsupported + bool SetDisplayName(const std::string& /*display_name*/) { return false; } + + bool SetProgram(const std::string& /*executable_path*/, + const std::vector& /*arguments*/) { + return false; + } + + std::string GetExecutablePath() const { return program_path_; } + std::vector GetArguments() const { return arguments_; } + + bool Enable() { return false; } + bool Disable() { return false; } + bool IsEnabled() const { return false; } + + private: + std::string id_; + std::string display_name_; + std::string program_path_; + std::vector arguments_; +}; + +// LaunchAtLogin public API forwarding to Impl + +LaunchAtLogin::LaunchAtLogin() : pimpl_(std::make_unique()) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id) : pimpl_(std::make_unique(id)) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id, const std::string& display_name) + : pimpl_(std::make_unique(id, display_name)) {} + +LaunchAtLogin::~LaunchAtLogin() = default; + +bool LaunchAtLogin::IsSupported() { + return Impl::IsSupported(); +} + +std::string LaunchAtLogin::GetId() const { + return pimpl_->GetId(); +} + +std::string LaunchAtLogin::GetDisplayName() const { + return pimpl_->GetDisplayName(); +} + +bool LaunchAtLogin::SetDisplayName(const std::string& display_name) { + return pimpl_->SetDisplayName(display_name); +} + +bool LaunchAtLogin::SetProgram(const std::string& executable_path, + const std::vector& arguments) { + return pimpl_->SetProgram(executable_path, arguments); +} + +std::string LaunchAtLogin::GetExecutablePath() const { + return pimpl_->GetExecutablePath(); +} + +std::vector LaunchAtLogin::GetArguments() const { + return pimpl_->GetArguments(); +} + +bool LaunchAtLogin::Enable() { + return pimpl_->Enable(); +} + +bool LaunchAtLogin::Disable() { + return pimpl_->Disable(); +} + +bool LaunchAtLogin::IsEnabled() const { + return pimpl_->IsEnabled(); +} + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/menu_item_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/menu_item_ohos.cpp new file mode 100644 index 0000000..9997d0c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/menu_item_ohos.cpp @@ -0,0 +1,115 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include "../../menu.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class MenuItem::Impl { + public: + Impl() = default; + + void* native_item_ = nullptr; + std::string label_; + MenuItemType type_ = MenuItemType::Normal; +}; + +MenuItem::MenuItem(const std::string& label, MenuItemType type) : pimpl_(std::make_unique()) { + pimpl_->label_ = label; + pimpl_->type_ = type; +} + +MenuItem::MenuItem(void* native_item) : pimpl_(std::make_unique()) { + pimpl_->native_item_ = native_item; +} + +MenuItem::~MenuItem() = default; + +MenuItemId MenuItem::GetId() const { + return IdAllocator::kInvalidId; +} + +MenuItemType MenuItem::GetType() const { + return pimpl_->type_; +} + +void MenuItem::SetLabel(const std::optional& label) { + pimpl_->label_ = label.has_value() ? label.value() : ""; +} + +std::optional MenuItem::GetLabel() const { + return pimpl_->label_.empty() ? std::nullopt : std::make_optional(pimpl_->label_); +} + +void MenuItem::SetIcon(std::shared_ptr image) { + // Not implemented on OpenHarmony yet +} + +std::shared_ptr MenuItem::GetIcon() const { + return nullptr; +} + +void MenuItem::SetTooltip(const std::optional& tooltip) { + // Not implemented on OpenHarmony yet +} + +std::optional MenuItem::GetTooltip() const { + return std::nullopt; +} + +void MenuItem::SetAccelerator(const std::optional& accelerator) { + // Not implemented on OpenHarmony yet +} + +KeyboardAccelerator MenuItem::GetAccelerator() const { + return KeyboardAccelerator(""); +} + +void MenuItem::SetEnabled(bool enabled) { + // Not implemented on OpenHarmony yet +} + +bool MenuItem::IsEnabled() const { + return true; +} + +void MenuItem::SetState(MenuItemState state) { + // Not implemented on OpenHarmony yet +} + +MenuItemState MenuItem::GetState() const { + return MenuItemState::Unchecked; +} + +void MenuItem::SetRadioGroup(int group_id) { + // Not implemented on OpenHarmony yet +} + +int MenuItem::GetRadioGroup() const { + return -1; +} + +void MenuItem::SetSubmenu(std::shared_ptr submenu) { + // Not implemented on OpenHarmony yet +} + +std::shared_ptr MenuItem::GetSubmenu() const { + return nullptr; +} + +void* MenuItem::GetNativeObjectInternal() const { + return pimpl_->native_item_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/menu_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/menu_ohos.cpp new file mode 100644 index 0000000..0de6392 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/menu_ohos.cpp @@ -0,0 +1,103 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include "../../menu.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class Menu::Impl { + public: + Impl() = default; + + void* native_menu_ = nullptr; +}; + +Menu::Menu() : pimpl_(std::make_unique()) {} + +Menu::Menu(void* native_menu) : pimpl_(std::make_unique()) { + pimpl_->native_menu_ = native_menu; +} + +Menu::~Menu() = default; + +MenuId Menu::GetId() const { + return IdAllocator::kInvalidId; +} + +void Menu::AddItem(std::shared_ptr item) { + // Not implemented on OpenHarmony yet +} + +void Menu::InsertItem(size_t index, std::shared_ptr item) { + // Not implemented on OpenHarmony yet +} + +bool Menu::RemoveItem(std::shared_ptr item) { + // Not implemented on OpenHarmony yet + return false; +} + +bool Menu::RemoveItemById(MenuItemId item_id) { + // Not implemented on OpenHarmony yet + return false; +} + +bool Menu::RemoveItemAt(size_t index) { + // Not implemented on OpenHarmony yet + return false; +} + +void Menu::Clear() { + // Not implemented on OpenHarmony yet +} + +void Menu::AddSeparator() { + // Not implemented on OpenHarmony yet +} + +void Menu::InsertSeparator(size_t index) { + // Not implemented on OpenHarmony yet +} + +size_t Menu::GetItemCount() const { + return 0; +} + +std::shared_ptr Menu::GetItemAt(size_t index) const { + return nullptr; +} + +std::shared_ptr Menu::GetItemById(MenuItemId item_id) const { + return nullptr; +} + +std::vector> Menu::GetAllItems() const { + return {}; +} + +bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { + // Not implemented on OpenHarmony yet + return false; +} + +bool Menu::Close() { + // Not implemented on OpenHarmony yet + return false; +} + +void* Menu::GetNativeObjectInternal() const { + return pimpl_->native_menu_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/message_dialog_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/message_dialog_ohos.cpp new file mode 100644 index 0000000..0294f69 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/message_dialog_ohos.cpp @@ -0,0 +1,85 @@ +#include "../../dialog.h" +#include "../../message_dialog.h" + +namespace nativeapi { + +// Private implementation class for MessageDialog (OHOS stub) +class MessageDialog::Impl { + public: + Impl(const std::string& title, const std::string& message) : title_(title), message_(message) { + // TODO: Implement HarmonyOS dialog using ArkUI + // Should use OHOS::Ace::DialogProperties or similar API + } + + ~Impl() { + // TODO: Cleanup if needed + } + + void SetTitle(const std::string& title) { title_ = title; } + + std::string GetTitle() const { return title_; } + + void SetMessage(const std::string& message) { message_ = message; } + + std::string GetMessage() const { return message_; } + + bool Open(DialogModality modality) { + // TODO: Implement using HarmonyOS ArkUI dialog + // Should use OHOS dialog APIs when available + // For now, return false (not implemented) + return false; + } + + bool Close() { + // TODO: Implement closing logic + return false; + } + + private: + std::string title_; + std::string message_; +}; + +// MessageDialog implementation +MessageDialog::MessageDialog(const std::string& title, const std::string& message) + : pimpl_(std::make_unique(title, message)) { + // Set default modality to None (non-modal) + SetModality(DialogModality::None); +} + +MessageDialog::~MessageDialog() = default; + +void MessageDialog::SetTitle(const std::string& title) { + pimpl_->SetTitle(title); +} + +std::string MessageDialog::GetTitle() const { + return pimpl_->GetTitle(); +} + +void MessageDialog::SetMessage(const std::string& message) { + pimpl_->SetMessage(message); +} + +std::string MessageDialog::GetMessage() const { + return pimpl_->GetMessage(); +} + +DialogModality MessageDialog::GetModality() const { + return modality_; +} + +void MessageDialog::SetModality(DialogModality modality) { + modality_ = modality; +} + +bool MessageDialog::Open() { + DialogModality modality = GetModality(); + return pimpl_->Open(modality); +} + +bool MessageDialog::Close() { + return pimpl_->Close(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/preferences_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/preferences_ohos.cpp new file mode 100644 index 0000000..61e28e7 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/preferences_ohos.cpp @@ -0,0 +1,103 @@ +#include "../../preferences.h" + +namespace nativeapi { + +class Preferences::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + const std::string& GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +Preferences::Preferences() : Preferences("default") {} + +Preferences::Preferences(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +Preferences::~Preferences() = default; + +// Interface implementation +bool Preferences::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string Preferences::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool Preferences::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool Preferences::Clear() { + return pimpl_->Clear(); +} + +bool Preferences::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector Preferences::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t Preferences::GetSize() const { + return pimpl_->GetSize(); +} + +std::map Preferences::GetAll() const { + return pimpl_->GetAll(); +} + +std::string Preferences::GetScope() const { + return pimpl_->GetScope(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/secure_storage_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/secure_storage_ohos.cpp new file mode 100644 index 0000000..dffffc6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/secure_storage_ohos.cpp @@ -0,0 +1,107 @@ +#include "../../secure_storage.h" + +namespace nativeapi { + +class SecureStorage::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + std::string GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +SecureStorage::SecureStorage() : SecureStorage("default") {} + +SecureStorage::SecureStorage(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +SecureStorage::~SecureStorage() = default; + +bool SecureStorage::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string SecureStorage::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool SecureStorage::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool SecureStorage::Clear() { + return pimpl_->Clear(); +} + +bool SecureStorage::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector SecureStorage::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t SecureStorage::GetSize() const { + return pimpl_->GetSize(); +} + +std::map SecureStorage::GetAll() const { + return pimpl_->GetAll(); +} + +std::string SecureStorage::GetScope() const { + return pimpl_->GetScope(); +} + +bool SecureStorage::IsAvailable() { + // Stub implementation - report as unavailable + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/shortcut_manager_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/shortcut_manager_ohos.cpp new file mode 100644 index 0000000..3541859 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/shortcut_manager_ohos.cpp @@ -0,0 +1,27 @@ +#include "../../shortcut_manager.h" + +namespace nativeapi { + +class ShortcutManagerImpl final : public ShortcutManager::Impl { + public: + explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager) {} + ~ShortcutManagerImpl() override = default; + + bool IsSupported() override { return false; } + bool RegisterShortcut(const std::shared_ptr& /*shortcut*/) override { return false; } + bool UnregisterShortcut(const std::shared_ptr& /*shortcut*/) override { return false; } + void SetupEventMonitoring() override {} + void CleanupEventMonitoring() override {} + + private: + ShortcutManager* manager_; +}; + +ShortcutManager::ShortcutManager() + : pimpl_(std::make_unique(this)), next_shortcut_id_(1), enabled_(true) {} + +ShortcutManager::~ShortcutManager() { + UnregisterAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/tray_icon_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/tray_icon_ohos.cpp new file mode 100644 index 0000000..24bcb2f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/tray_icon_ohos.cpp @@ -0,0 +1,113 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include "../../tray_icon.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class TrayIcon::Impl { + public: + Impl() = default; + + void* native_tray_ = nullptr; +}; + +TrayIcon::TrayIcon() : pimpl_(std::make_unique()) {} + +TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique()) { + pimpl_->native_tray_ = tray; +} + +TrayIcon::~TrayIcon() = default; + +TrayIconId TrayIcon::GetId() { + return IdAllocator::kInvalidId; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + // Not implemented on OpenHarmony yet +} + +std::shared_ptr TrayIcon::GetIcon() const { + return nullptr; +} + +void TrayIcon::SetTitle(std::optional title) { + // Not implemented on OpenHarmony yet +} + +std::optional TrayIcon::GetTitle() { + return std::nullopt; +} + +void TrayIcon::SetTooltip(std::optional tooltip) { + // Not implemented on OpenHarmony yet +} + +std::optional TrayIcon::GetTooltip() { + return std::nullopt; +} + +void TrayIcon::SetContextMenu(std::shared_ptr menu) { + // Not implemented on OpenHarmony yet +} + +std::shared_ptr TrayIcon::GetContextMenu() { + return nullptr; +} + +void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { + // Not implemented on OpenHarmony yet +} + +ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { + return ContextMenuTrigger::None; +} + +Rectangle TrayIcon::GetBounds() { + return Rectangle{0.0, 0.0, 0.0, 0.0}; +} + +bool TrayIcon::SetVisible(bool visible) { + // Not implemented on OpenHarmony yet + return false; +} + +bool TrayIcon::IsVisible() { + return false; +} + +bool TrayIcon::OpenContextMenu() { + // Not implemented on OpenHarmony yet + return false; +} + +bool TrayIcon::CloseContextMenu() { + // Not implemented on OpenHarmony yet + return false; +} + +void TrayIcon::StartEventListening() { + // Not implemented on OpenHarmony yet +} + +void TrayIcon::StopEventListening() { + // Not implemented on OpenHarmony yet +} + +void* TrayIcon::GetNativeObjectInternal() const { + return pimpl_->native_tray_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/tray_manager_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/tray_manager_ohos.cpp new file mode 100644 index 0000000..31f32de --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/tray_manager_ohos.cpp @@ -0,0 +1,52 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include +#include "../../tray_manager.h" + +#ifdef __OHOS__ +#define LOG_CORE 0xD001700 +// Note: LOG_TAG is defined in hilog/log.h as NULL, +// redefine to avoid warnings if needed +#undef LOG_TAG +#define LOG_TAG "NativeApi" +#endif + +namespace nativeapi { + +class TrayManager::Impl { + public: + Impl(TrayManager* manager) : manager_(manager) {} + + TrayManager* manager_; +}; + +TrayManager::TrayManager() : pimpl_(std::make_unique(this)), next_tray_id_(1) {} + +TrayManager::~TrayManager() = default; + +bool TrayManager::IsSupported() { + // Not implemented on OpenHarmony yet + return false; +} + +std::shared_ptr TrayManager::Get(TrayIconId id) { + std::lock_guard lock(mutex_); + auto it = trays_.find(id); + return (it != trays_.end()) ? it->second : nullptr; +} + +std::vector> TrayManager::GetAll() { + std::lock_guard lock(mutex_); + std::vector> result; + result.reserve(trays_.size()); + for (const auto& [id, tray] : trays_) { + result.push_back(tray); + } + return result; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/url_opener_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/url_opener_ohos.cpp new file mode 100644 index 0000000..207a018 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/url_opener_ohos.cpp @@ -0,0 +1,26 @@ +#include "../../url_opener.h" + +namespace nativeapi { +namespace { + +class OhosUrlOpenerImpl final : public UrlOpener::Impl { + public: + bool IsSupported() const override { return false; } + + UrlOpenResult Open(const std::string& url) const override { + (void)url; + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kUnsupportedPlatform; + result.error_message = "URL opening is not implemented on OHOS in this native layer."; + return result; + } +}; + +} // namespace + +UrlOpener::UrlOpener() : pimpl_(std::make_unique()) {} + +UrlOpener::~UrlOpener() = default; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/window_manager_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/window_manager_ohos.cpp new file mode 100644 index 0000000..a77dcf2 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/window_manager_ohos.cpp @@ -0,0 +1,153 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" + +namespace nativeapi { + +// Helper function to manage mapping between window pointers and WindowIds +static WindowId GetOrCreateWindowId(void* native_window) { + if (!native_window) { + return IdAllocator::kInvalidId; + } + + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + auto it = window_id_map.find(native_window); + if (it != window_id_map.end()) { + return it->second; + } + + // Allocate new ID using the IdAllocator + WindowId new_id = IdAllocator::Allocate(); + if (new_id != IdAllocator::kInvalidId) { + window_id_map[native_window] = new_id; + } + return new_id; +} + +// Helper function to find window by WindowId +static void* FindNativeWindowById(WindowId id) { + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + for (const auto& pair : window_id_map) { + if (pair.second == id) { + return pair.first; + } + } + return nullptr; +} + +// Private implementation for OpenHarmony +class WindowManager::Impl { + public: + Impl(WindowManager* manager) : manager_(manager) {} + ~Impl() {} + + void StartEventListening() { + // On OpenHarmony, event monitoring is done through Ability callbacks + // Window event monitoring setup + } + + void StopEventListening() { + // Window event monitoring cleanup + } + + private: + WindowManager* manager_; +}; + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + StartEventListening(); +} + +WindowManager::~WindowManager() { + StopEventListening(); +} + +std::shared_ptr WindowManager::Get(WindowId id) { + auto cached = WindowRegistry::GetInstance().Get(id); + if (cached) { + return cached; + } + + // Try to find the window by ID + void* native_window = FindNativeWindowById(id); + if (native_window) { + auto window = std::make_shared(native_window); + WindowRegistry::GetInstance().Add(id, window); + return window; + } + + return nullptr; +} + +std::vector> WindowManager::GetAll() { + return WindowRegistry::GetInstance().GetAll(); +} + +std::shared_ptr WindowManager::GetCurrent() { + // On OpenHarmony, the current window is typically the Ability's window + auto all = WindowRegistry::GetInstance().GetAll(); + return all.empty() ? nullptr : all.front(); +} + +void WindowManager::SetWillShowHook(std::optional hook) { + // Empty implementation +} + +void WindowManager::SetWillHideHook(std::optional hook) { + // Empty implementation +} + +bool WindowManager::HasWillShowHook() const { + return false; +} + +bool WindowManager::HasWillHideHook() const { + return false; +} + +void WindowManager::HandleWillShow(WindowId id) { + // Empty implementation +} + +void WindowManager::HandleWillHide(WindowId id) { + // Empty implementation +} + +bool WindowManager::CallOriginalShow(WindowId id) { + // OpenHarmony doesn't support swizzling for window show/hide + // Return false to indicate unsupported + return false; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + // OpenHarmony doesn't support swizzling for window show/hide + // Return false to indicate unsupported + return false; +} + +void WindowManager::StartEventListening() { + pimpl_->StartEventListening(); +} + +void WindowManager::StopEventListening() { + pimpl_->StopEventListening(); +} + +void WindowManager::DispatchWindowEvent(const WindowEvent& event) { + Emit(event); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/ohos/window_ohos.cpp b/packages/cnativeapi/cxx_impl/src/platform/ohos/window_ohos.cpp new file mode 100644 index 0000000..26e7493 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ohos/window_ohos.cpp @@ -0,0 +1,366 @@ +#ifdef __OHOS__ +#include +#endif +#include +#include +#include +#include +#include "../../foundation/id_allocator.h" +#include "../../window.h" +#include "../../window_manager.h" + +namespace nativeapi { + +// Private implementation class +class Window::Impl { + public: + Impl(void* window) : native_window_(window), visual_effect_(VisualEffect::None) {} + void* native_window_; + VisualEffect visual_effect_; +}; + +Window::Window() : pimpl_(std::make_unique(nullptr)) {} + +Window::Window(void* window) : pimpl_(std::make_unique(window)) {} + +Window::~Window() {} + +WindowId Window::GetId() const { + if (!pimpl_->native_window_) { + return IdAllocator::kInvalidId; + } + + // Store the allocated ID in a static map to ensure consistency + static std::unordered_map window_id_map; + static std::mutex map_mutex; + + std::lock_guard lock(map_mutex); + auto it = window_id_map.find(pimpl_->native_window_); + if (it != window_id_map.end()) { + return it->second; + } + + // Allocate new ID using the IdAllocator + WindowId new_id = IdAllocator::Allocate(); + if (new_id != IdAllocator::kInvalidId) { + window_id_map[pimpl_->native_window_] = new_id; + } + return new_id; +} + +void Window::Focus() { + if (pimpl_->native_window_) { + // On OpenHarmony, focus is managed by the Ability lifecycle + // Window focus requested + } +} + +void Window::Blur() { + if (pimpl_->native_window_) { + // On OpenHarmony, blur is managed by the Ability lifecycle + // Window blur requested + } +} + +bool Window::IsFocused() const { + // OpenHarmony manages focus through the Ability lifecycle + return pimpl_->native_window_ != nullptr; +} + +void Window::Show() { + if (pimpl_->native_window_) { + // On OpenHarmony, visibility is managed by the Ability lifecycle + // Window show requested + } +} + +void Window::ShowInactive() { + if (pimpl_->native_window_) { + Show(); + } +} + +void Window::Hide() { + if (pimpl_->native_window_) { + // On OpenHarmony, visibility is managed by the Ability lifecycle + // Window hide requested + } +} + +bool Window::IsVisible() const { + return pimpl_->native_window_ != nullptr; +} + +void Window::Maximize() { + // Maximize is not applicable to OpenHarmony Abilities + // Maximize not supported on OpenHarmony +} + +void Window::Unmaximize() { + // Unmaximize is not applicable to OpenHarmony Abilities + // Unmaximize not supported on OpenHarmony +} + +bool Window::IsMaximized() const { + return false; +} + +void Window::Minimize() { + // On OpenHarmony, this would move the Ability to background + if (pimpl_->native_window_) { + // Window minimize requested + } +} + +void Window::Restore() { + // On OpenHarmony, restore would bring Ability to foreground + if (pimpl_->native_window_) { + // Window restore requested + } +} + +bool Window::IsMinimized() const { + return false; +} + +void Window::SetFullScreen(bool is_full_screen) { + // On OpenHarmony, fullscreen is managed through window properties + if (pimpl_->native_window_) { + // Fullscreen set + } +} + +bool Window::IsFullScreen() const { + return false; +} + +void Window::SetBounds(Rectangle bounds) { + if (pimpl_->native_window_) { + // SetBounds called + } +} + +Rectangle Window::GetBounds() const { + if (!pimpl_->native_window_) { + return Rectangle{0.0, 0.0, 0.0, 0.0}; + } + + // Default bounds for OpenHarmony + return Rectangle{0.0, 0.0, 360.0, 780.0}; +} + +void Window::SetSize(Size size, bool animate) { + if (pimpl_->native_window_) { + // SetSize called + } +} + +Size Window::GetSize() const { + if (!pimpl_->native_window_) { + return Size{0.0, 0.0}; + } + + return Size{360.0, 780.0}; +} + +void Window::SetContentSize(Size size) { + SetSize(size, false); +} + +Size Window::GetContentSize() const { + return GetSize(); +} + +void Window::SetContentBounds(Rectangle bounds) { + // On OpenHarmony, content bounds is the same as window bounds + SetBounds(bounds); +} + +Rectangle Window::GetContentBounds() const { + // On OpenHarmony, content bounds is the same as window bounds + return GetBounds(); +} + +void Window::SetMinimumSize(Size size) { + // SetMinimumSize not fully supported on OpenHarmony +} + +Size Window::GetMinimumSize() const { + return Size{0, 0}; +} + +void Window::SetMaximumSize(Size size) { + // SetMaximumSize not fully supported on OpenHarmony +} + +Size Window::GetMaximumSize() const { + return Size{0, 0}; +} + +void Window::SetResizable(bool is_resizable) { + // SetResizable not supported on OpenHarmony +} + +bool Window::IsResizable() const { + return false; +} + +void Window::SetMovable(bool is_movable) { + // SetMovable not supported on OpenHarmony +} + +bool Window::IsMovable() const { + return false; +} + +void Window::SetMinimizable(bool is_minimizable) { + // SetMinimizable not supported on OpenHarmony +} + +bool Window::IsMinimizable() const { + return true; +} + +void Window::SetMaximizable(bool is_maximizable) { + // SetMaximizable not supported on OpenHarmony +} + +bool Window::IsMaximizable() const { + return false; +} + +void Window::SetFullScreenable(bool is_full_screenable) { + // SetFullScreenable not supported on OpenHarmony +} + +bool Window::IsFullScreenable() const { + return true; +} + +void Window::SetClosable(bool is_closable) { + // SetClosable not supported on OpenHarmony +} + +bool Window::IsClosable() const { + return true; +} + +void Window::SetWindowControlButtonsVisible(bool is_visible) { + // Not applicable to OpenHarmony - mobile apps don't have window control buttons +} + +bool Window::IsWindowControlButtonsVisible() const { + // Not applicable to OpenHarmony - mobile apps don't have window control buttons + return false; +} + +void Window::SetAlwaysOnTop(bool is_always_on_top) { + // SetAlwaysOnTop not fully supported on OpenHarmony +} + +bool Window::IsAlwaysOnTop() const { + return false; +} + +void Window::SetPosition(Point point) { + // SetPosition not supported on OpenHarmony +} + +Point Window::GetPosition() const { + return Point{0, 0}; +} + +void Window::Center() { + // On OpenHarmony, window positioning is not supported + // Abilities are automatically managed by the system + if (pimpl_->native_window_) { + // Center not supported on OpenHarmony - Abilities are managed by the system + } +} + +void Window::SetTitle(std::string title) { + // SetTitle not supported on OpenHarmony (use Ability title) +} + +std::string Window::GetTitle() const { + return ""; +} + +void Window::SetTitleBarStyle(TitleBarStyle style) { + // SetTitleBarStyle not supported on OpenHarmony (use system bar APIs) +} + +TitleBarStyle Window::GetTitleBarStyle() const { + return TitleBarStyle::Normal; +} + +void Window::SetHasShadow(bool has_shadow) { + // SetHasShadow not supported on OpenHarmony +} + +bool Window::HasShadow() const { + return false; +} + +void Window::SetOpacity(float opacity) { + // SetOpacity not supported on OpenHarmony +} + +float Window::GetOpacity() const { + return 1.0f; +} + +void Window::SetVisualEffect(VisualEffect effect) { + pimpl_->visual_effect_ = effect; + // SetVisualEffect not supported on OpenHarmony +} + +VisualEffect Window::GetVisualEffect() const { + return pimpl_->visual_effect_; +} + +void Window::SetBackgroundColor(const Color& color) { + // SetBackgroundColor not supported on OpenHarmony +} + +Color Window::GetBackgroundColor() const { + return Color::White; +} + +void Window::SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces) { + // SetVisibleOnAllWorkspaces not supported on OpenHarmony +} + +bool Window::IsVisibleOnAllWorkspaces() const { + return false; +} + +void Window::SetIgnoreMouseEvents(bool is_ignore_mouse_events) { + // SetIgnoreMouseEvents not supported on OpenHarmony +} + +bool Window::IsIgnoreMouseEvents() const { + return false; +} + +void Window::SetFocusable(bool is_focusable) { + // SetFocusable not supported on OpenHarmony +} + +bool Window::IsFocusable() const { + return true; +} + +void Window::StartDragging() { + // StartDragging not supported on OpenHarmony +} + +void Window::StartResizing() { + // StartResizing not supported on OpenHarmony +} + +void* Window::GetNativeObjectInternal() const { + return pimpl_->native_window_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/accessibility_manager_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/accessibility_manager_windows.cpp new file mode 100644 index 0000000..c72848b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/accessibility_manager_windows.cpp @@ -0,0 +1,26 @@ +#include + +#include "../../accessibility_manager.h" + +namespace nativeapi { + +void AccessibilityManager::Enable() { + // On Windows, accessibility features are typically enabled through system + // settings This is a placeholder implementation that doesn't perform actual + // enabling In a real implementation, you might need to: + // - Check if accessibility APIs are available + // - Request appropriate permissions if needed + // - Enable specific accessibility features +} + +bool AccessibilityManager::IsEnabled() { + // On Windows, you can check various accessibility settings + // This is a basic implementation that always returns true + // In a real implementation, you might check: + // - SystemParametersInfo with accessibility parameters + // - Registry settings for accessibility features + // - UI Automation availability + return true; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/application_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/application_windows.cpp new file mode 100644 index 0000000..1e1ab96 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/application_windows.cpp @@ -0,0 +1,257 @@ +// clang-format off +#include +#include +// clang-format on +#include +#include +#include + +#include "../../application.h" +#include "../../menu.h" +#include "../../window_manager.h" + +namespace nativeapi { + +class Application::Impl { + public: + Impl(Application* app) : app_(app), hinstance_(GetModuleHandle(nullptr)) {} + ~Impl() = default; + + bool Initialize() { + // Initialize COM + HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (FAILED(hr)) { + return false; + } + + return true; + } + + int Run() { + MSG msg = {}; + int exit_code = 0; + + while (true) { + // Get message from the message queue + BOOL result = GetMessage(&msg, nullptr, 0, 0); + + if (result == -1) { + // Error occurred + exit_code = -1; + break; + } else if (result == 0) { + // WM_QUIT received + exit_code = static_cast(msg.wParam); + break; + } else { + // Translate and dispatch the message + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + return exit_code; + } + + int Run(std::shared_ptr window) { + if (!window) { + return -1; + } + + // Set the window as primary window + app_->SetPrimaryWindow(window); + + // Show the window + window->Show(); + window->Focus(); + + // Start the message loop + MSG msg = {}; + int exit_code = 0; + + while (true) { + // Get message from the message queue + BOOL result = GetMessage(&msg, nullptr, 0, 0); + + if (result == -1) { + // Error occurred + exit_code = -1; + break; + } else if (result == 0) { + // WM_QUIT received + exit_code = static_cast(msg.wParam); + break; + } else { + // Translate and dispatch the message + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + return exit_code; + } + + void Quit(int exit_code) { PostQuitMessage(exit_code); } + + bool SetIcon(const std::string& icon_path) { + if (icon_path.empty()) { + return false; + } + + // Convert to wide string + std::wstring wide_path(icon_path.begin(), icon_path.end()); + + // Load icon from file using LoadImageW for wide strings + HICON icon = static_cast( + LoadImageW(nullptr, wide_path.c_str(), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE)); + + if (!icon) { + return false; + } + + // Set application icon + SetClassLongPtr(GetConsoleWindow(), GCLP_HICON, reinterpret_cast(icon)); + + return true; + } + + bool SetDockIconVisible(bool visible) { + // Windows doesn't have a dock, so this is a no-op + return true; + } + + bool SetMenuBar(std::shared_ptr menu) { + if (!menu) { + return false; + } + + // Get the primary window + auto primary_window = app_->GetPrimaryWindow(); + if (!primary_window) { + return false; + } + + // Get the native window handle + HWND hwnd = static_cast(primary_window->GetNativeObject()); + if (!hwnd) { + return false; + } + + // Get the native menu handle + HMENU hmenu = static_cast(menu->GetNativeObject()); + if (!hmenu) { + return false; + } + + // Set the menu for the window + SetMenu(hwnd, hmenu); + + return true; + } + + void CleanupEventMonitoring() { + // Clean up Windows-specific event monitoring + if (mutex_) { + CloseHandle(mutex_); + mutex_ = nullptr; + } + + CoUninitialize(); + } + + private: + Application* app_; + HINSTANCE hinstance_; + HANDLE mutex_ = nullptr; +}; + +Application::Application() + : initialized_(true), running_(false), exit_code_(0), pimpl_(std::make_unique(this)) { + // Perform platform-specific initialization automatically + pimpl_->Initialize(); + + // Emit application started event + Emit(); +} + +Application::~Application() { + // Clean up platform-specific event monitoring + pimpl_->CleanupEventMonitoring(); +} + +int Application::Run() { + running_ = true; + + // Start the platform-specific main event loop + int result = pimpl_->Run(); + + running_ = false; + + // Emit exit event + Emit(result); + + return result; +} + +int Application::Run(std::shared_ptr window) { + if (!window) { + return -1; // Invalid window + } + + running_ = true; + + // Start the platform-specific main event loop with window + int result = pimpl_->Run(window); + + running_ = false; + + // Emit exit event + Emit(result); + + return result; +} + +void Application::Quit(int exit_code) { + exit_code_ = exit_code; + + // Emit quit requested event + Emit(); + + // Request platform-specific quit + pimpl_->Quit(exit_code); +} + +bool Application::IsRunning() const { + return running_; +} + +bool Application::IsSingleInstance() const { + return false; +} + +bool Application::SetIcon(const std::string& icon_path) { + return pimpl_->SetIcon(icon_path); +} + +bool Application::SetDockIconVisible(bool visible) { + return pimpl_->SetDockIconVisible(visible); +} + +bool Application::SetMenuBar(std::shared_ptr menu) { + return pimpl_->SetMenuBar(menu); +} + +std::shared_ptr Application::GetPrimaryWindow() const { + return primary_window_; +} + +void Application::SetPrimaryWindow(std::shared_ptr window) { + primary_window_ = window; +} + +std::vector> Application::GetAllWindows() const { + auto& window_manager = WindowManager::GetInstance(); + return window_manager.GetAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/dispatcher_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/dispatcher_windows.cpp new file mode 100644 index 0000000..a58bd7b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/dispatcher_windows.cpp @@ -0,0 +1,120 @@ +#include "../../foundation/dispatcher_platform.h" +#include "../../foundation/dispatcher_common.h" + +#include + +#include + +namespace nativeapi { +namespace dispatcher_platform { + +namespace { + +// Private message carrying a heap-allocated std::function* in lParam. +constexpr UINT kDispatchMessage = WM_APP + 0x51; +constexpr wchar_t kDispatchWindowClass[] = L"NativeApiDispatcherWindow"; + +HWND g_dispatch_window = nullptr; +std::once_flag g_dispatch_window_once; + +LRESULT CALLBACK DispatchWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + if (msg == kDispatchMessage) { + auto* work = reinterpret_cast*>(lparam); + if (work) { + (*work)(); + delete work; + } + return 0; + } + return DefWindowProcW(hwnd, msg, wparam, lparam); +} + +/** + * Creates the message-only window that receives dispatched work. + * + * MUST run on the main thread: a window's messages are delivered to the message + * queue of the thread that created it, so creating it anywhere else would defeat + * the entire purpose. Callers are responsible for the thread check. + * + * Deliberately lazy rather than created during static initialization — under a + * DLL build that would run inside the loader lock, where creating a window can + * deadlock. + */ +void EnsureDispatchWindowOnMainThread() { + std::call_once(g_dispatch_window_once, [] { + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = DispatchWndProc; + wc.hInstance = GetModuleHandleW(nullptr); + wc.lpszClassName = kDispatchWindowClass; + RegisterClassExW(&wc); + + g_dispatch_window = CreateWindowExW(0, kDispatchWindowClass, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, + nullptr, wc.hInstance, nullptr); + }); +} + +} // namespace + +bool PlatformIsMainThread() { + const bool is_main = dispatcher_internal::IsMainThreadByCapturedId(); + if (is_main) { + // Prime the dispatch window while we are provably on the right thread, so + // that a later post from a worker thread has somewhere to go. + EnsureDispatchWindowOnMainThread(); + } + return is_main; +} + +void PlatformSetMainThread() { + dispatcher_internal::CaptureCallerAsMainThread(); + EnsureDispatchWindowOnMainThread(); +} + +bool PlatformIsMainThreadDispatchSupported() { + return true; +} + +bool PlatformRunOnMainThread(std::function fn) { + if (!fn) { + return true; + } + + if (dispatcher_internal::IsMainThreadByCapturedId()) { + EnsureDispatchWindowOnMainThread(); + } + + if (!g_dispatch_window) { + // A worker thread asked to dispatch before the main thread ever touched the + // library, so there is no window yet and we must not create one here. + // Report the failure instead of silently dropping the work; callers should + // call SetMainThread() during startup to make this impossible. + return false; + } + + auto* work = new std::function(std::move(fn)); + if (!PostMessageW(g_dispatch_window, kDispatchMessage, 0, reinterpret_cast(work))) { + delete work; + return false; + } + return true; +} + +bool PlatformRunMainThreadLoopFor(int timeout_ms) { + const DWORD deadline = GetTickCount() + static_cast(timeout_ms); + MSG msg; + for (;;) { + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + if (timeout_ms <= 0 || GetTickCount() >= deadline) { + return true; + } + // Sleep until a message arrives or the budget expires, whichever is first. + MsgWaitForMultipleObjects(0, nullptr, FALSE, deadline - GetTickCount(), QS_ALLINPUT); + } +} + +} // namespace dispatcher_platform +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/display_manager_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/display_manager_windows.cpp new file mode 100644 index 0000000..bfd2d59 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/display_manager_windows.cpp @@ -0,0 +1,62 @@ +#include +#include +#include + +#include "../../display.h" +#include "../../display_manager.h" +#include "dpi_utils_windows.h" +#include "string_utils_windows.h" + +namespace nativeapi { + +DisplayManager::DisplayManager() { + // Prime the instance cache so the first change notification diffs against + // the displays present at startup. + GetAll(); + // TODO: Set up display configuration change monitoring + // On Windows, you would typically register for WM_DISPLAYCHANGE messages + // and call HandleDisplaysChanged() from the handler. +} + +DisplayManager::~DisplayManager() { + // TODO: Clean up display change monitoring +} + +std::vector DisplayManager::EnumerateNativeDisplays() { + std::vector natives; + auto enumProc = [](HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, + LPARAM dwData) -> BOOL { + auto* out = reinterpret_cast*>(dwData); + + MONITORINFOEXW monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFOEXW); + + if (GetMonitorInfoW(hMonitor, &monitorInfo)) { + bool isPrimary = (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) != 0; + // The device name is stable across configuration changes, unlike the + // HMONITOR value, so it serves as the identity key. + out->push_back({WCharArrayToString(monitorInfo.szDevice), hMonitor, isPrimary}); + } + + return TRUE; + }; + EnumDisplayMonitors(nullptr, nullptr, enumProc, reinterpret_cast(&natives)); + return natives; +} + +Point DisplayManager::GetCursorPosition() { + POINT cursorPos; + if (GetCursorPos(&cursorPos)) { + // Determine which monitor the cursor is on for DPI scaling + HMONITOR hMonitor = + MonitorFromPoint(cursorPos, MONITOR_DEFAULTTONEAREST); + double scale = GetScaleFactorForMonitor(hMonitor); + if (scale <= 0.0) + scale = 1.0; + return {static_cast(cursorPos.x) / scale, + static_cast(cursorPos.y) / scale}; + } + return {0.0, 0.0}; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/display_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/display_windows.cpp new file mode 100644 index 0000000..b166684 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/display_windows.cpp @@ -0,0 +1,120 @@ +#include "../../display.h" + +#include +#include "dpi_utils_windows.h" +#include "string_utils_windows.h" + +namespace nativeapi { + +// Private implementation class +class Display::Impl { + public: + Impl() = default; + Impl(HMONITOR monitor) : h_monitor_(monitor) {} + + const DisplayId id_ = IdAllocator::Allocate(); + HMONITOR h_monitor_ = nullptr; +}; + +Display::Display(void* display) : pimpl_(std::make_unique()) { + if (display) { + pimpl_->h_monitor_ = (HMONITOR)display; + } +} + +Display::~Display() = default; + +void* Display::GetNativeObjectInternal() const { + return pimpl_->h_monitor_; +} + +// Helper function to get monitor info +MONITORINFOEXW GetMonitorInfoEx(HMONITOR hMonitor) { + MONITORINFOEXW monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFOEXW); + GetMonitorInfoW(hMonitor, &monitorInfo); + return monitorInfo; +} + +// Getters - directly read from HMONITOR +DisplayId Display::GetId() const { + return pimpl_->id_; +} + +std::string Display::GetName() const { + if (!pimpl_->h_monitor_) + return ""; + MONITORINFOEXW monitorInfo = GetMonitorInfoEx(pimpl_->h_monitor_); + return WCharArrayToString(monitorInfo.szDevice); +} + +Point Display::GetPosition() const { + if (!pimpl_->h_monitor_) + return {0.0, 0.0}; + MONITORINFOEXW monitorInfo = GetMonitorInfoEx(pimpl_->h_monitor_); + RECT rect = monitorInfo.rcMonitor; + double scale = GetScaleFactorForMonitor(pimpl_->h_monitor_); + if (scale <= 0.0) + scale = 1.0; + return {static_cast(rect.left) / scale, + static_cast(rect.top) / scale}; +} + +Size Display::GetSize() const { + if (!pimpl_->h_monitor_) + return {0.0, 0.0}; + MONITORINFOEXW monitorInfo = GetMonitorInfoEx(pimpl_->h_monitor_); + RECT rect = monitorInfo.rcMonitor; + double scale = GetScaleFactorForMonitor(pimpl_->h_monitor_); + if (scale <= 0.0) + scale = 1.0; + return {static_cast(rect.right - rect.left) / scale, + static_cast(rect.bottom - rect.top) / scale}; +} + +Rectangle Display::GetWorkArea() const { + if (!pimpl_->h_monitor_) + return {0.0, 0.0, 0.0, 0.0}; + MONITORINFOEXW monitorInfo = GetMonitorInfoEx(pimpl_->h_monitor_); + RECT workRect = monitorInfo.rcWork; + double scale = GetScaleFactorForMonitor(pimpl_->h_monitor_); + if (scale <= 0.0) + scale = 1.0; + return {static_cast(workRect.left) / scale, + static_cast(workRect.top) / scale, + static_cast(workRect.right - workRect.left) / scale, + static_cast(workRect.bottom - workRect.top) / scale}; +} + +double Display::GetScaleFactor() const { + if (!pimpl_->h_monitor_) + return 1.0; + double scale = GetScaleFactorForMonitor(pimpl_->h_monitor_); + return (scale > 0.0) ? scale : 1.0; +} + +bool Display::IsPrimary() const { + if (!pimpl_->h_monitor_) + return false; + MONITORINFOEXW monitorInfo = GetMonitorInfoEx(pimpl_->h_monitor_); + return (monitorInfo.dwFlags & MONITORINFOF_PRIMARY) != 0; +} + +DisplayOrientation Display::GetOrientation() const { + if (!pimpl_->h_monitor_) + return DisplayOrientation::kPortrait; + Size size = GetSize(); + return (size.width > size.height) ? DisplayOrientation::kLandscape + : DisplayOrientation::kPortrait; +} + +int Display::GetRefreshRate() const { + return 60; // Default refresh rate, would need additional Windows APIs to get + // actual value +} + +int Display::GetBitDepth() const { + return 32; // Default bit depth for modern displays +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.cpp new file mode 100644 index 0000000..9e9afcd --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.cpp @@ -0,0 +1,69 @@ +#include + +namespace nativeapi { + +// Internal: per-monitor DPI via Shcore when available +double GetScaleFactorForMonitor(HMONITOR hmonitor) { + if (!hmonitor) + return 1.0; + typedef HRESULT(WINAPI * GetDpiForMonitorFunc)(HMONITOR, int, UINT*, UINT*); + static GetDpiForMonitorFunc pGetDpiForMonitor = nullptr; + static bool resolved = false; + if (!resolved) { + HMODULE hShcore = LoadLibraryW(L"Shcore.dll"); + if (hShcore) { + pGetDpiForMonitor = + reinterpret_cast(GetProcAddress(hShcore, "GetDpiForMonitor")); + } + resolved = true; + } + if (pGetDpiForMonitor) { + UINT dpiX = 96, dpiY = 96; + if (SUCCEEDED(pGetDpiForMonitor(hmonitor, 0 /* MDT_EFFECTIVE_DPI */, &dpiX, &dpiY))) { + return static_cast(dpiX) / 96.0; + } + } + return 1.0; +} + +double GetScaleFactorForWindow(HWND hwnd) { + if (hwnd) { + // Prefer GetDpiForWindow if available + typedef UINT(WINAPI * GetDpiForWindowFunc)(HWND); + static GetDpiForWindowFunc pGetDpiForWindow = nullptr; + static bool resolved_win = false; + if (!resolved_win) { + HMODULE hUser32 = LoadLibraryW(L"user32.dll"); + if (hUser32) { + pGetDpiForWindow = + reinterpret_cast(GetProcAddress(hUser32, "GetDpiForWindow")); + } + resolved_win = true; + } + if (pGetDpiForWindow) { + UINT dpi = pGetDpiForWindow(hwnd); + if (dpi > 0) { + return static_cast(dpi) / 96.0; + } + } + + // Fallback: per-monitor DPI + HMONITOR hmonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + double monitor_scale = GetScaleFactorForMonitor(hmonitor); + if (monitor_scale > 0.0) + return monitor_scale; + } + + // Fallback: system DPI + HDC hdc = GetDC(nullptr); + if (hdc) { + int dpiX = GetDeviceCaps(hdc, LOGPIXELSX); + ReleaseDC(nullptr, hdc); + if (dpiX > 0) { + return static_cast(dpiX) / 96.0; + } + } + return 1.0; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.h b/packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.h new file mode 100644 index 0000000..1ebd702 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/dpi_utils_windows.h @@ -0,0 +1,12 @@ +#pragma once +#include + +namespace nativeapi { + +// Returns the DPI scale factor for the given window (1.0 at 96 DPI) +double GetScaleFactorForWindow(HWND hwnd); + +// Returns the DPI scale factor for the given monitor (1.0 at 96 DPI) +double GetScaleFactorForMonitor(HMONITOR hmonitor); + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/image_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/image_windows.cpp new file mode 100644 index 0000000..eab9f17 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/image_windows.cpp @@ -0,0 +1,430 @@ +#include +#include +#include +#include +#include +#include +#include "../../foundation/geometry.h" +#include "../../image.h" + +#pragma comment(lib, "gdiplus.lib") + +namespace nativeapi { + +// Forward declaration for Windows-specific helper function +HICON ImageToHICON(const Image* image, int width, int height); + +// Helper function to convert std::string to std::wstring +static std::wstring StringToWString(const std::string& str) { + if (str.empty()) + return std::wstring(); + int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), NULL, 0); + std::wstring wstrTo(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), &wstrTo[0], size_needed); + return wstrTo; +} + +// Helper function to get image encoder CLSID +static int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) { + UINT num = 0; // number of image encoders + UINT size = 0; // size of the image encoder array in bytes + + Gdiplus::GetImageEncodersSize(&num, &size); + if (size == 0) + return -1; + + Gdiplus::ImageCodecInfo* pImageCodecInfo = (Gdiplus::ImageCodecInfo*)(malloc(size)); + if (pImageCodecInfo == NULL) + return -1; + + Gdiplus::GetImageEncoders(num, size, pImageCodecInfo); + + for (UINT j = 0; j < num; ++j) { + if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) { + *pClsid = pImageCodecInfo[j].Clsid; + free(pImageCodecInfo); + return j; + } + } + + free(pImageCodecInfo); + return -1; +} + +// Windows-specific implementation of Image class using GDI+ +class Image::Impl { + public: + Gdiplus::Bitmap* bitmap_; + std::string source_; + Size size_; + std::string format_; + + Impl() : bitmap_(nullptr), size_({0, 0}), format_("Unknown") {} + + ~Impl() { + if (bitmap_) { + delete bitmap_; + } + } + + Impl(const Impl& other) + : bitmap_(nullptr), source_(other.source_), size_(other.size_), format_(other.format_) { + if (other.bitmap_) { + bitmap_ = other.bitmap_->Clone(0, 0, other.bitmap_->GetWidth(), other.bitmap_->GetHeight(), + other.bitmap_->GetPixelFormat()); + } + } + + Impl& operator=(const Impl& other) { + if (this != &other) { + if (bitmap_) { + delete bitmap_; + } + bitmap_ = nullptr; + source_ = other.source_; + size_ = other.size_; + format_ = other.format_; + if (other.bitmap_) { + bitmap_ = other.bitmap_->Clone(0, 0, other.bitmap_->GetWidth(), other.bitmap_->GetHeight(), + other.bitmap_->GetPixelFormat()); + } + } + return *this; + } +}; + +// Static GDI+ initialization +static bool g_gdiplus_initialized = false; +static ULONG_PTR g_gdiplus_token = 0; + +static void EnsureGdiplusInitialized() { + if (!g_gdiplus_initialized) { + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + Gdiplus::GdiplusStartup(&g_gdiplus_token, &gdiplusStartupInput, NULL); + g_gdiplus_initialized = true; + } +} + +Image::Image() : pimpl_(std::make_unique()) { + EnsureGdiplusInitialized(); +} + +Image::~Image() = default; + +Image::Image(const Image& other) : pimpl_(std::make_unique(*other.pimpl_)) {} + +Image::Image(Image&& other) noexcept : pimpl_(std::move(other.pimpl_)) {} + +std::shared_ptr Image::FromFile(const std::string& file_path) { + EnsureGdiplusInitialized(); + auto image = std::shared_ptr(new Image()); + + std::wstring wFilePath = StringToWString(file_path); + Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromFile(wFilePath.c_str()); + + if (bitmap && bitmap->GetLastStatus() == Gdiplus::Ok) { + image->pimpl_->bitmap_ = bitmap; + image->pimpl_->source_ = file_path; + + // Get actual image size + UINT width = bitmap->GetWidth(); + UINT height = bitmap->GetHeight(); + image->pimpl_->size_ = {static_cast(width), static_cast(height)}; + + // Determine format from file extension + size_t dotPos = file_path.find_last_of('.'); + if (dotPos != std::string::npos) { + std::string extension = file_path.substr(dotPos + 1); + // Convert to lowercase + for (auto& c : extension) { + c = std::tolower(c); + } + + if (extension == "png") { + image->pimpl_->format_ = "PNG"; + } else if (extension == "jpg" || extension == "jpeg") { + image->pimpl_->format_ = "JPEG"; + } else if (extension == "gif") { + image->pimpl_->format_ = "GIF"; + } else if (extension == "bmp") { + image->pimpl_->format_ = "BMP"; + } else if (extension == "tiff" || extension == "tif") { + image->pimpl_->format_ = "TIFF"; + } else if (extension == "ico") { + image->pimpl_->format_ = "ICO"; + } else { + image->pimpl_->format_ = "Unknown"; + } + } + } else { + if (bitmap) { + delete bitmap; + } + return nullptr; + } + + return image; +} + +// Helper function to decode base64 +static std::vector DecodeBase64(const std::string& base64_data) { + std::vector result; + + // Calculate the expected output size + DWORD dwOutLen = 0; + if (!CryptStringToBinaryA(base64_data.c_str(), 0, CRYPT_STRING_BASE64, NULL, &dwOutLen, NULL, + NULL)) { + return result; + } + + result.resize(dwOutLen); + if (!CryptStringToBinaryA(base64_data.c_str(), 0, CRYPT_STRING_BASE64, result.data(), &dwOutLen, + NULL, NULL)) { + result.clear(); + } + + return result; +} + +std::shared_ptr Image::FromBase64(const std::string& base64_data) { + EnsureGdiplusInitialized(); + auto image = std::shared_ptr(new Image()); + + // Remove data URI prefix if present + std::string cleanBase64 = base64_data; + size_t commaPos = base64_data.find(','); + if (commaPos != std::string::npos) { + cleanBase64 = base64_data.substr(commaPos + 1); + } + + // Decode base64 + std::vector imageData = DecodeBase64(cleanBase64); + + if (!imageData.empty()) { + // Create IStream from memory + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, imageData.size()); + if (hMem) { + void* pMem = GlobalLock(hMem); + if (pMem) { + memcpy(pMem, imageData.data(), imageData.size()); + GlobalUnlock(hMem); + + IStream* pStream = nullptr; + if (CreateStreamOnHGlobal(hMem, TRUE, &pStream) == S_OK) { + Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromStream(pStream); + pStream->Release(); + + if (bitmap && bitmap->GetLastStatus() == Gdiplus::Ok) { + image->pimpl_->bitmap_ = bitmap; + image->pimpl_->source_ = base64_data; + + // Get actual image size + UINT width = bitmap->GetWidth(); + UINT height = bitmap->GetHeight(); + image->pimpl_->size_ = {static_cast(width), static_cast(height)}; + + // Default assumption for base64 images + image->pimpl_->format_ = "PNG"; + } else { + if (bitmap) { + delete bitmap; + } + return nullptr; + } + } else { + GlobalFree(hMem); + return nullptr; + } + } else { + GlobalFree(hMem); + return nullptr; + } + } else { + return nullptr; + } + } else { + return nullptr; + } + + return image; +} + +Size Image::GetSize() const { + return pimpl_->size_; +} + +std::string Image::GetFormat() const { + return pimpl_->format_; +} + +// Helper function to encode to base64 +static std::string EncodeBase64(const unsigned char* data, size_t length) { + DWORD dwOutLen = 0; + DWORD dwLength = static_cast(length); + if (!CryptBinaryToStringA((BYTE*)data, dwLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, + &dwOutLen)) { + return ""; + } + + std::string result(dwOutLen, '\0'); + if (!CryptBinaryToStringA((BYTE*)data, dwLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + &result[0], &dwOutLen)) { + return ""; + } + + // Remove trailing null characters + result.resize(dwOutLen - 1); + return result; +} + +std::string Image::ToBase64() const { + if (!pimpl_->bitmap_) { + return ""; + } + + // Create IStream to save to memory + IStream* pStream = nullptr; + if (CreateStreamOnHGlobal(NULL, TRUE, &pStream) != S_OK) { + return ""; + } + + // Get PNG encoder + CLSID pngClsid; + if (GetEncoderClsid(L"image/png", &pngClsid) < 0) { + pStream->Release(); + return ""; + } + + // Save to stream + Gdiplus::Status status = pimpl_->bitmap_->Save(pStream, &pngClsid, NULL); + if (status != Gdiplus::Ok) { + pStream->Release(); + return ""; + } + + // Get stream size + STATSTG statstg; + if (pStream->Stat(&statstg, STATFLAG_DEFAULT) != S_OK) { + pStream->Release(); + return ""; + } + + // Read stream data + LARGE_INTEGER li = {0}; + pStream->Seek(li, STREAM_SEEK_SET, NULL); + + std::vector buffer(statstg.cbSize.LowPart); + ULONG bytesRead = 0; + pStream->Read(buffer.data(), statstg.cbSize.LowPart, &bytesRead); + pStream->Release(); + + if (bytesRead == 0) { + return ""; + } + + // Convert to base64 + std::string base64String = EncodeBase64(buffer.data(), bytesRead); + return "data:image/png;base64," + base64String; +} + +bool Image::SaveToFile(const std::string& file_path) const { + if (!pimpl_->bitmap_) { + return false; + } + + // Determine file type from extension + size_t dotPos = file_path.find_last_of('.'); + std::wstring mimeType = L"image/png"; // default + + if (dotPos != std::string::npos) { + std::string extension = file_path.substr(dotPos + 1); + // Convert to lowercase + for (auto& c : extension) { + c = std::tolower(c); + } + + if (extension == "jpg" || extension == "jpeg") { + mimeType = L"image/jpeg"; + } else if (extension == "png") { + mimeType = L"image/png"; + } else if (extension == "bmp") { + mimeType = L"image/bmp"; + } else if (extension == "gif") { + mimeType = L"image/gif"; + } else if (extension == "tiff" || extension == "tif") { + mimeType = L"image/tiff"; + } + } + + // Get encoder CLSID + CLSID encoderClsid; + if (GetEncoderClsid(mimeType.c_str(), &encoderClsid) < 0) { + return false; + } + + // Convert file path to wide string + std::wstring wFilePath = StringToWString(file_path); + + // Set quality for JPEG + Gdiplus::EncoderParameters encoderParams; + ULONG quality = 90; + + if (mimeType == L"image/jpeg") { + encoderParams.Count = 1; + encoderParams.Parameter[0].Guid = Gdiplus::EncoderQuality; + encoderParams.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong; + encoderParams.Parameter[0].NumberOfValues = 1; + encoderParams.Parameter[0].Value = &quality; + + Gdiplus::Status status = + pimpl_->bitmap_->Save(wFilePath.c_str(), &encoderClsid, &encoderParams); + return status == Gdiplus::Ok; + } else { + Gdiplus::Status status = pimpl_->bitmap_->Save(wFilePath.c_str(), &encoderClsid, NULL); + return status == Gdiplus::Ok; + } +} + +void* Image::GetNativeObjectInternal() const { + return pimpl_->bitmap_; +} + +// Windows-specific helper function to convert Image to HICON +// Uses only the public GetNativeObject() API to avoid private access +HICON ImageToHICON(const Image* image, int width, int height) { + if (!image) { + return nullptr; + } + + // Retrieve native bitmap via public API + void* native = image->GetNativeObject(); + Gdiplus::Bitmap* bitmap = static_cast(native); + if (!bitmap) { + return nullptr; + } + + // Scale bitmap if necessary + Gdiplus::Bitmap* scaledBitmap = bitmap; + bool needsScaling = (bitmap->GetWidth() != static_cast(width) || + bitmap->GetHeight() != static_cast(height)); + + if (needsScaling) { + scaledBitmap = new Gdiplus::Bitmap(width, height, bitmap->GetPixelFormat()); + Gdiplus::Graphics graphics(scaledBitmap); + graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); + graphics.DrawImage(bitmap, 0, 0, width, height); + } + + // Convert to HICON + HICON hIcon = nullptr; + scaledBitmap->GetHICON(&hIcon); + + // Clean up scaled bitmap if we created one + if (needsScaling && scaledBitmap) { + delete scaledBitmap; + } + + return hIcon; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/keyboard_monitor_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/keyboard_monitor_windows.cpp new file mode 100644 index 0000000..4445156 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/keyboard_monitor_windows.cpp @@ -0,0 +1,125 @@ +#include +#include +#include + +#include "../../foundation/keyboard.h" +#include "../../keyboard_monitor.h" + +namespace nativeapi { + +class KeyboardMonitor::Impl { + public: + Impl(KeyboardMonitor* monitor) : monitor_(monitor), hook_(nullptr) {} + + HHOOK hook_; + KeyboardMonitor* monitor_; +}; + +KeyboardMonitor::KeyboardMonitor() : impl_(std::make_unique(this)) {} + +KeyboardMonitor::~KeyboardMonitor() { + Stop(); +} + +// Global pointer to current keyboard monitor instance +static KeyboardMonitor* g_current_monitor = nullptr; + +// Low-level keyboard hook procedure +static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode >= 0) { + KBDLLHOOKSTRUCT* pKeyboard = reinterpret_cast(lParam); + + // Get the KeyboardMonitor instance from global variable + if (!g_current_monitor) { + return CallNextHookEx(nullptr, nCode, wParam, lParam); + } + + auto& emitter = g_current_monitor->GetInternalEventEmitter(); + + if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { + KeyPressedEvent key_event(pKeyboard->vkCode); + emitter.Emit(key_event); + } else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) { + KeyReleasedEvent key_event(pKeyboard->vkCode); + emitter.Emit(key_event); + } + + // Check for modifier key changes + uint32_t modifier_keys = static_cast(ModifierKey::None); + + if (GetAsyncKeyState(VK_SHIFT) & 0x8000) { + modifier_keys |= static_cast(ModifierKey::Shift); + } + if (GetAsyncKeyState(VK_CONTROL) & 0x8000) { + modifier_keys |= static_cast(ModifierKey::Ctrl); + } + if (GetAsyncKeyState(VK_MENU) & 0x8000) { + modifier_keys |= static_cast(ModifierKey::Alt); + } + if (GetAsyncKeyState(VK_LWIN) & 0x8000 || GetAsyncKeyState(VK_RWIN) & 0x8000) { + modifier_keys |= static_cast(ModifierKey::Meta); + } + if (GetAsyncKeyState(VK_CAPITAL) & 0x0001) { + modifier_keys |= static_cast(ModifierKey::CapsLock); + } + if (GetAsyncKeyState(VK_NUMLOCK) & 0x0001) { + modifier_keys |= static_cast(ModifierKey::NumLock); + } + if (GetAsyncKeyState(VK_SCROLL) & 0x0001) { + modifier_keys |= static_cast(ModifierKey::ScrollLock); + } + + static uint32_t last_modifier_keys = 0; + if (modifier_keys != last_modifier_keys) { + ModifierKeysChangedEvent modifier_event(modifier_keys); + emitter.Emit(modifier_event); + last_modifier_keys = modifier_keys; + } + } + + return CallNextHookEx(nullptr, nCode, wParam, lParam); +} + +void KeyboardMonitor::Start() { + if (impl_->hook_ != nullptr) { + return; // Already started + } + + // Set up the global reference for the hook procedure + g_current_monitor = this; + + // Install low-level keyboard hook + impl_->hook_ = + SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(nullptr), 0); + + if (impl_->hook_ == nullptr) { + std::cerr << "Failed to install keyboard hook. Error: " << GetLastError() << std::endl; + return; + } +} + +void KeyboardMonitor::Stop() { + if (impl_->hook_ == nullptr) { + return; // Already stopped + } + + // Clear the global reference + g_current_monitor = nullptr; + + // Uninstall the hook + if (UnhookWindowsHookEx(impl_->hook_)) { + impl_->hook_ = nullptr; + } else { + std::cerr << "Failed to uninstall keyboard hook. Error: " << GetLastError() << std::endl; + } +} + +bool KeyboardMonitor::IsMonitoring() const { + return impl_->hook_ != nullptr; +} + +EventEmitter& KeyboardMonitor::GetInternalEventEmitter() { + return *this; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/launch_at_login_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/launch_at_login_windows.cpp new file mode 100644 index 0000000..90bbf74 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/launch_at_login_windows.cpp @@ -0,0 +1,287 @@ +#include + +#include +#include +#include +#include +#include + +#include "../../launch_at_login.h" + +namespace nativeapi { + +namespace { + +// Get the absolute path to the current executable (ANSI). +static std::string DetectDefaultProgramPath() { + char path[MAX_PATH] = {0}; + DWORD len = GetModuleFileNameA(nullptr, path, static_cast(sizeof(path))); + if (len == 0 || len >= sizeof(path)) { + return std::string(); + } + return std::string(path, len); +} + +static std::string Basename(const std::string& path) { + if (path.empty()) + return std::string(); + size_t pos = path.find_last_of("\\/"); + if (pos == std::string::npos) + return path; + if (pos + 1 >= path.size()) + return path; // trailing slash + return path.substr(pos + 1); +} + +static std::string StripExtension(const std::string& name) { + size_t pos = name.find_last_of('.'); + if (pos == std::string::npos) + return name; + return name.substr(0, pos); +} + +// Default identifier for Windows; consistent with other platforms' pattern. +static std::string DetectDefaultId() { + std::string prog = DetectDefaultProgramPath(); + std::string name = StripExtension(Basename(prog)); + if (name.empty()) + name = "app"; + return "com.nativeapi.launch_at_login." + name; +} + +static std::string DetectDefaultDisplayName() { + std::string prog = DetectDefaultProgramPath(); + std::string name = StripExtension(Basename(prog)); + if (name.empty()) + name = "Application"; + return name; +} + +// Determine if a Windows command argument needs quoting. +static bool NeedsQuoting(const std::string& s) { + if (s.empty()) + return true; + for (char c : s) { + if (std::isspace(static_cast(c)) || c == '"' || c == '\t') { + return true; + } + } + return false; +} + +// Quote a single argument for Windows command-line per CRT parsing rules. +// Reference: https://learn.microsoft.com/en-us/cpp/cpp/parsing-c-command-line-arguments +static std::string QuoteArgWindows(const std::string& arg) { + if (!NeedsQuoting(arg)) { + return arg; + } + + std::string result; + result.push_back('"'); + + size_t i = 0; + while (i < arg.size()) { + // Count number of backslashes before next character + size_t backslash_count = 0; + while (i < arg.size() && arg[i] == '\\') { + ++backslash_count; + ++i; + } + + if (i == arg.size()) { + // Escape all backslashes at the end + result.append(backslash_count * 2, '\\'); + break; + } + + if (arg[i] == '"') { + // Escape all backslashes (double them), then escape the quote + result.append(backslash_count * 2 + 1, '\\'); + result.push_back('"'); + } else { + // Just copy the backslashes + result.append(backslash_count, '\\'); + result.push_back(arg[i]); + } + ++i; + } + + result.push_back('"'); + return result; +} + +// Build full command line: "C:\Path To\app.exe" "arg1" "arg 2" +static std::string BuildCommandLine(const std::string& program, + const std::vector& args) { + std::ostringstream oss; + oss << QuoteArgWindows(program); + for (const auto& a : args) { + oss << ' ' << QuoteArgWindows(a); + } + return oss.str(); +} + +// Open (or create) HKCU\Software\Microsoft\Windows\CurrentVersion\Run key for write. +static bool OpenRunKeyWrite(HKEY& hkey) { + const char* kRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + DWORD disposition = 0; + LONG res = RegCreateKeyExA(HKEY_CURRENT_USER, kRunKey, 0, NULL, REG_OPTION_NON_VOLATILE, + KEY_SET_VALUE | KEY_WRITE, NULL, &hkey, &disposition); + return res == ERROR_SUCCESS; +} + +// Open HKCU\Software\Microsoft\Windows\CurrentVersion\Run for read. +static bool OpenRunKeyRead(HKEY& hkey) { + const char* kRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + LONG res = RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_READ, &hkey); + return res == ERROR_SUCCESS; +} + +} // namespace + +class LaunchAtLogin::Impl { + public: + static bool IsSupported() { return true; } + + Impl() + : id_(DetectDefaultId()), + display_name_(DetectDefaultDisplayName()), + program_path_(DetectDefaultProgramPath()) {} + + explicit Impl(const std::string& id) + : id_(id), + display_name_(DetectDefaultDisplayName()), + program_path_(DetectDefaultProgramPath()) {} + + Impl(const std::string& id, const std::string& display_name) + : id_(id), display_name_(display_name), program_path_(DetectDefaultProgramPath()) {} + + ~Impl() = default; + + std::string GetId() const { return id_; } + + std::string GetDisplayName() const { return display_name_; } + + bool SetDisplayName(const std::string& display_name) { + display_name_ = display_name; + return true; + } + + bool SetProgram(const std::string& executable_path, const std::vector& arguments) { + program_path_ = executable_path; + arguments_ = arguments; + return true; + } + + std::string GetExecutablePath() const { return program_path_; } + + std::vector GetArguments() const { return arguments_; } + + bool Enable() { + if (program_path_.empty()) { + program_path_ = DetectDefaultProgramPath(); + if (program_path_.empty()) { + return false; + } + } + + const std::string cmd = BuildCommandLine(program_path_, arguments_); + + HKEY hkey = nullptr; + if (!OpenRunKeyWrite(hkey)) { + return false; + } + + LONG res = + RegSetValueExA(hkey, id_.c_str(), 0, REG_SZ, reinterpret_cast(cmd.c_str()), + static_cast(cmd.size() + 1)); + RegCloseKey(hkey); + return res == ERROR_SUCCESS; + } + + bool Disable() { + HKEY hkey = nullptr; + // Use KEY_SET_VALUE to delete the value + const char* kRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + LONG open_res = RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_SET_VALUE, &hkey); + if (open_res != ERROR_SUCCESS) { + // Consider it disabled if the key doesn't exist + return open_res == ERROR_FILE_NOT_FOUND; + } + + LONG del_res = RegDeleteValueA(hkey, id_.c_str()); + RegCloseKey(hkey); + return (del_res == ERROR_SUCCESS) || (del_res == ERROR_FILE_NOT_FOUND); + } + + bool IsEnabled() const { + HKEY hkey = nullptr; + if (!OpenRunKeyRead(hkey)) { + return false; + } + // Check if a value with name id_ exists + LONG res = RegQueryValueExA(hkey, id_.c_str(), NULL, NULL, NULL, NULL); + RegCloseKey(hkey); + return res == ERROR_SUCCESS; + } + + private: + std::string id_; + std::string display_name_; + std::string program_path_; + std::vector arguments_; +}; + +// LaunchAtLogin public API implementations + +LaunchAtLogin::LaunchAtLogin() : pimpl_(std::make_unique()) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id) : pimpl_(std::make_unique(id)) {} + +LaunchAtLogin::LaunchAtLogin(const std::string& id, const std::string& display_name) + : pimpl_(std::make_unique(id, display_name)) {} + +LaunchAtLogin::~LaunchAtLogin() = default; + +bool LaunchAtLogin::IsSupported() { + return Impl::IsSupported(); +} + +std::string LaunchAtLogin::GetId() const { + return pimpl_->GetId(); +} + +std::string LaunchAtLogin::GetDisplayName() const { + return pimpl_->GetDisplayName(); +} + +bool LaunchAtLogin::SetDisplayName(const std::string& display_name) { + return pimpl_->SetDisplayName(display_name); +} + +bool LaunchAtLogin::SetProgram(const std::string& executable_path, + const std::vector& arguments) { + return pimpl_->SetProgram(executable_path, arguments); +} + +std::string LaunchAtLogin::GetExecutablePath() const { + return pimpl_->GetExecutablePath(); +} + +std::vector LaunchAtLogin::GetArguments() const { + return pimpl_->GetArguments(); +} + +bool LaunchAtLogin::Enable() { + return pimpl_->Enable(); +} + +bool LaunchAtLogin::Disable() { + return pimpl_->Disable(); +} + +bool LaunchAtLogin::IsEnabled() const { + return pimpl_->IsEnabled(); +} + +} // namespace nativeapi \ No newline at end of file diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/menu_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/menu_windows.cpp new file mode 100644 index 0000000..54e2f76 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/menu_windows.cpp @@ -0,0 +1,827 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../foundation/id_allocator.h" +#include "../../image.h" +#include "../../menu.h" +#include "../../window.h" +#include "dpi_utils_windows.h" +#include "string_utils_windows.h" +#include "window_message_dispatcher.h" + +namespace nativeapi { + +HICON ImageToHICON(const Image* image, int width, int height); + +// Helper function to convert KeyboardAccelerator to Windows accelerator +std::pair ConvertAccelerator(const KeyboardAccelerator& accelerator) { + UINT key = 0; + UINT modifiers = 0; + + // Convert key + if (!accelerator.key.empty()) { + if (accelerator.key.length() == 1) { + // Single character key + char c = std::toupper(accelerator.key[0]); + key = static_cast(c); + } else { + // Special keys + std::string key_str = accelerator.key; + if (key_str == "F1") + key = VK_F1; + else if (key_str == "F2") + key = VK_F2; + else if (key_str == "F3") + key = VK_F3; + else if (key_str == "F4") + key = VK_F4; + else if (key_str == "F5") + key = VK_F5; + else if (key_str == "F6") + key = VK_F6; + else if (key_str == "F7") + key = VK_F7; + else if (key_str == "F8") + key = VK_F8; + else if (key_str == "F9") + key = VK_F9; + else if (key_str == "F10") + key = VK_F10; + else if (key_str == "F11") + key = VK_F11; + else if (key_str == "F12") + key = VK_F12; + else if (key_str == "Enter" || key_str == "Return") + key = VK_RETURN; + else if (key_str == "Tab") + key = VK_TAB; + else if (key_str == "Space") + key = VK_SPACE; + else if (key_str == "Escape") + key = VK_ESCAPE; + else if (key_str == "Delete" || key_str == "Backspace") + key = VK_BACK; + else if (key_str == "ArrowUp") + key = VK_UP; + else if (key_str == "ArrowDown") + key = VK_DOWN; + else if (key_str == "ArrowLeft") + key = VK_LEFT; + else if (key_str == "ArrowRight") + key = VK_RIGHT; + } + } + + // Convert modifiers + if ((accelerator.modifiers & ModifierKey::Ctrl) != ModifierKey::None) { + modifiers |= FCONTROL; + } + if ((accelerator.modifiers & ModifierKey::Alt) != ModifierKey::None) { + modifiers |= FALT; + } + if ((accelerator.modifiers & ModifierKey::Shift) != ModifierKey::None) { + modifiers |= FSHIFT; + } + // Note: Windows doesn't have a direct equivalent for Meta key in accelerators + + return std::make_pair(key, modifiers); +} + +// MenuItem::Impl implementation +class MenuItem::Impl { + public: + MenuItemId id_; + HMENU parent_menu_; + MenuItemType type_; + std::optional label_; + std::shared_ptr image_; + HICON menu_icon_; // Stored icon for menu item with transparency support + HBITMAP menu_bitmap_; // Stored 32-bit ARGB bitmap with icon and transparent background + std::optional tooltip_; + KeyboardAccelerator accelerator_; + bool has_accelerator_; + MenuItemState state_; + bool enabled_; + int radio_group_; + std::shared_ptr submenu_; + int window_proc_handle_id_; + std::function clicked_callback_; + size_t submenu_opened_listener_id_; + size_t submenu_closed_listener_id_; + + Impl(MenuItemId id, HMENU parent_menu, MenuItemType type) + : id_(id), + parent_menu_(parent_menu), + type_(type), + menu_icon_(nullptr), + menu_bitmap_(nullptr), + accelerator_("", ModifierKey::None), + has_accelerator_(false), + state_(MenuItemState::Unchecked), + enabled_(true), + radio_group_(-1), + window_proc_handle_id_(-1), + submenu_opened_listener_id_(0), + submenu_closed_listener_id_(0) {} + + ~Impl() { + // Unregister window procedure handler + if (window_proc_handle_id_ != -1) { + WindowMessageDispatcher::GetInstance().UnregisterHandler(window_proc_handle_id_); + } + + // Remove submenu listeners before cleaning up submenu reference + if (submenu_ && submenu_opened_listener_id_ != 0) { + submenu_->RemoveListener(submenu_opened_listener_id_); + submenu_opened_listener_id_ = 0; + } + if (submenu_ && submenu_closed_listener_id_ != 0) { + submenu_->RemoveListener(submenu_closed_listener_id_); + submenu_closed_listener_id_ = 0; + } + + // Clean up menu icon + if (menu_icon_) { + DestroyIcon(menu_icon_); + menu_icon_ = nullptr; + } + + // Clean up menu bitmap + if (menu_bitmap_) { + DeleteObject(menu_bitmap_); + menu_bitmap_ = nullptr; + } + + // Windows menu items are automatically cleaned up when the menu is + // destroyed + } + + // Handle window procedure delegate for menu item clicks + std::optional HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_COMMAND) { + // For WM_COMMAND from menus, wparam contains the menu item ID + // When using popup menus (TrackPopupMenu), the full 32-bit ID is + // preserved in wparam, unlike menu bars which only use 16-bit IDs + if (lparam == 0) { + // Reconstruct the full 32-bit menu item ID from wparam + MenuItemId menu_item_id = static_cast(wparam); + + // Check if this is our menu item + if (menu_item_id == id_) { + std::cout << "MenuItem: Item clicked, ID = " << menu_item_id << std::endl; + // Call the clicked callback to emit the event + if (clicked_callback_) { + clicked_callback_(id_); + } + return 0; + } + } + } + return std::nullopt; // Let other handlers process + } +}; + +MenuItem::MenuItem(void* native_item) + : pimpl_(std::make_unique(IdAllocator::Allocate(), + nullptr, + MenuItemType::Normal)) { + // Set clicked callback to emit event + pimpl_->clicked_callback_ = [this](MenuItemId id) { Emit(id); }; + + // Register window procedure handler for menu item clicks + HWND host_window = WindowMessageDispatcher::GetInstance().GetHostWindow(); + if (host_window) { + pimpl_->window_proc_handle_id_ = WindowMessageDispatcher::GetInstance().RegisterHandler( + host_window, [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return pimpl_->HandleWindowProc(hwnd, message, wparam, lparam); + }); + } +} + +MenuItem::MenuItem(const std::string& label, MenuItemType type) + : pimpl_(std::make_unique(IdAllocator::Allocate(), nullptr, type)) { + pimpl_->label_ = label; + + // Set clicked callback to emit event + pimpl_->clicked_callback_ = [this](MenuItemId id) { Emit(id); }; + + // Register window procedure handler for menu item clicks + HWND host_window = WindowMessageDispatcher::GetInstance().GetHostWindow(); + if (host_window) { + pimpl_->window_proc_handle_id_ = WindowMessageDispatcher::GetInstance().RegisterHandler( + host_window, [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return pimpl_->HandleWindowProc(hwnd, message, wparam, lparam); + }); + } +} + +MenuItem::~MenuItem() {} + +MenuItemId MenuItem::GetId() const { + return pimpl_->id_; +} + +MenuItemType MenuItem::GetType() const { + return pimpl_->type_; +} + +void MenuItem::SetLabel(const std::optional& label) { + pimpl_->label_ = label; + if (pimpl_->parent_menu_) { + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_STRING; + std::string label_str = label.has_value() ? *label : ""; + std::wstring w_label_str = StringToWString(label_str); + mii.dwTypeData = const_cast(w_label_str.c_str()); + SetMenuItemInfoW(pimpl_->parent_menu_, pimpl_->id_, FALSE, &mii); + } +} + +std::optional MenuItem::GetLabel() const { + return pimpl_->label_; +} + +void MenuItem::SetIcon(std::shared_ptr image) { + // Clean up previous resources + if (pimpl_->menu_icon_) { + DestroyIcon(pimpl_->menu_icon_); + pimpl_->menu_icon_ = nullptr; + } + if (pimpl_->menu_bitmap_) { + DeleteObject(pimpl_->menu_bitmap_); + pimpl_->menu_bitmap_ = nullptr; + } + + pimpl_->image_ = image; + + if (image && pimpl_->parent_menu_) { + // Use 32x32 for menu icons + const int iconSize = 32; + + // Convert image to HICON first for proper transparency support + HICON hIcon = ImageToHICON(image.get(), iconSize, iconSize); + + if (hIcon) { + pimpl_->menu_icon_ = hIcon; + + // Create a 32-bit ARGB bitmap + HDC hdc = GetDC(nullptr); + + // Create BITMAPINFO for 32-bit ARGB + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = iconSize; + bmi.bmiHeader.biHeight = -iconSize; // Negative for top-down DIB + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + void* pBits = nullptr; + HBITMAP hBmp = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0); + + if (hBmp && pBits) { + // Fill with fully transparent pixels (all-zero ARGB) + // This ensures the icon blends seamlessly with the menu background. + // On Windows Vista+, MIIM_BITMAP with a 32-bit ARGB DIB section + // respects the alpha channel for proper transparency. + DWORD* pixels = static_cast(pBits); + memset(pixels, 0, iconSize * iconSize * sizeof(DWORD)); + + // Draw the icon on the bitmap + HDC hdcMem = CreateCompatibleDC(hdc); + HBITMAP hOldBmp = (HBITMAP)SelectObject(hdcMem, hBmp); + + // Draw icon with proper blending + DrawIconEx(hdcMem, 0, 0, hIcon, iconSize, iconSize, 0, nullptr, DI_NORMAL); + + SelectObject(hdcMem, hOldBmp); + DeleteDC(hdcMem); + + // Store the bitmap for cleanup + pimpl_->menu_bitmap_ = hBmp; + + // Set the bitmap on the menu item + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_BITMAP; + mii.hbmpItem = hBmp; + SetMenuItemInfoW(pimpl_->parent_menu_, pimpl_->id_, FALSE, &mii); + } + + ReleaseDC(nullptr, hdc); + } + } +} + +std::shared_ptr MenuItem::GetIcon() const { + return pimpl_->image_; +} + +void MenuItem::SetTooltip(const std::optional& tooltip) { + pimpl_->tooltip_ = tooltip; + // Windows doesn't have built-in tooltip support for menu items + // This would require custom implementation +} + +std::optional MenuItem::GetTooltip() const { + return pimpl_->tooltip_; +} + +void MenuItem::SetAccelerator(const std::optional& accelerator) { + if (accelerator.has_value()) { + pimpl_->accelerator_ = *accelerator; + pimpl_->has_accelerator_ = true; + } else { + pimpl_->accelerator_ = KeyboardAccelerator("", ModifierKey::None); + pimpl_->has_accelerator_ = false; + } + // Windows accelerators would be handled through accelerator tables + // This is a placeholder implementation +} + +KeyboardAccelerator MenuItem::GetAccelerator() const { + if (pimpl_->has_accelerator_) { + return pimpl_->accelerator_; + } + return KeyboardAccelerator("", ModifierKey::None); +} + +void MenuItem::SetEnabled(bool enabled) { + pimpl_->enabled_ = enabled; + if (pimpl_->parent_menu_) { + EnableMenuItem(pimpl_->parent_menu_, pimpl_->id_, enabled ? MF_ENABLED : MF_GRAYED); + } +} + +bool MenuItem::IsEnabled() const { + return pimpl_->enabled_; +} + +void MenuItem::SetState(MenuItemState state) { + if (pimpl_->type_ == MenuItemType::Checkbox || pimpl_->type_ == MenuItemType::Radio) { + pimpl_->state_ = state; + + if (pimpl_->parent_menu_) { + UINT check_state = MF_UNCHECKED; + if (state == MenuItemState::Checked) { + check_state = MF_CHECKED; + } + CheckMenuItem(pimpl_->parent_menu_, pimpl_->id_, check_state); + } + + // Handle radio button group logic + if (pimpl_->type_ == MenuItemType::Radio && state == MenuItemState::Checked && + pimpl_->radio_group_ >= 0) { + // Note: Radio group logic would need to be implemented differently + // without global registry. This could be done by maintaining group + // information in the parent menu or through other means. + // For now, this functionality is disabled. + } + } +} + +MenuItemState MenuItem::GetState() const { + return pimpl_->state_; +} + +void MenuItem::SetRadioGroup(int group_id) { + pimpl_->radio_group_ = group_id; +} + +int MenuItem::GetRadioGroup() const { + return pimpl_->radio_group_; +} + +void MenuItem::SetSubmenu(std::shared_ptr submenu) { + // Remove previous submenu listeners if they exist + if (pimpl_->submenu_ && pimpl_->submenu_opened_listener_id_ != 0) { + pimpl_->submenu_->RemoveListener(pimpl_->submenu_opened_listener_id_); + pimpl_->submenu_opened_listener_id_ = 0; + } + if (pimpl_->submenu_ && pimpl_->submenu_closed_listener_id_ != 0) { + pimpl_->submenu_->RemoveListener(pimpl_->submenu_closed_listener_id_); + pimpl_->submenu_closed_listener_id_ = 0; + } + + pimpl_->submenu_ = submenu; + + // Update platform menu if parent_menu_ is set + if (pimpl_->parent_menu_ && submenu) { + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(MENUITEMINFOW); + mii.fMask = MIIM_SUBMENU; + mii.hSubMenu = static_cast(submenu->GetNativeObject()); + SetMenuItemInfoW(pimpl_->parent_menu_, pimpl_->id_, FALSE, &mii); + } + + // Add event listeners to forward submenu events (independent of parent_menu_) + if (submenu) { + MenuItemId menu_item_id = pimpl_->id_; + MenuItem* self = this; + pimpl_->submenu_opened_listener_id_ = + submenu->AddListener([self, menu_item_id](const MenuOpenedEvent& event) { + self->Emit(menu_item_id); + }); + + pimpl_->submenu_closed_listener_id_ = + submenu->AddListener([self, menu_item_id](const MenuClosedEvent& event) { + self->Emit(menu_item_id); + }); + } +} + +std::shared_ptr MenuItem::GetSubmenu() const { + return pimpl_->submenu_; +} + +void* MenuItem::GetNativeObjectInternal() const { + return reinterpret_cast(static_cast(pimpl_->id_)); +} + +// Menu::Impl implementation +class Menu::Impl { + public: + MenuId id_; + HMENU hmenu_; + std::vector> items_; + int window_proc_handle_id_; + std::function opened_callback_; + std::function closed_callback_; + + Impl(MenuId id, HMENU menu) : id_(id), hmenu_(menu), window_proc_handle_id_(-1) {} + + ~Impl() { + // Unregister window procedure handler + if (window_proc_handle_id_ != -1) { + WindowMessageDispatcher::GetInstance().UnregisterHandler(window_proc_handle_id_); + } + + if (hmenu_) { + DestroyMenu(hmenu_); + } + } + + // Handle window procedure delegate for menu lifecycle events + std::optional HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + // Handle menu lifecycle events only + // Menu item clicks are now handled by individual MenuItem instances + if (message == WM_INITMENUPOPUP) { + // wParam contains the HMENU handle of the popup menu being opened + HMENU popup_menu = reinterpret_cast(wparam); + + if (popup_menu == hmenu_) { + // This is our menu being opened + // Emit menu opened event via callback + if (opened_callback_) { + opened_callback_(id_); + } + } else { + // Check if this is a submenu of one of our items + for (const auto& item : items_) { + auto submenu = item->GetSubmenu(); + if (submenu && submenu->GetNativeObject() == popup_menu) { + // This is a submenu of one of our items being opened + // The MenuItem will handle emitting MenuItemSubmenuOpenedEvent + // through its event listeners + break; + } + } + } + } else if (message == WM_UNINITMENUPOPUP) { + // wParam contains the HMENU handle of the popup menu being closed + HMENU popup_menu = reinterpret_cast(wparam); + + if (popup_menu == hmenu_) { + // This is our menu being closed + // Emit menu closed event via callback + if (closed_callback_) { + closed_callback_(id_); + } + } else { + // Check if this is a submenu of one of our items + for (const auto& item : items_) { + auto submenu = item->GetSubmenu(); + if (submenu && submenu->GetNativeObject() == popup_menu) { + // This is a submenu of one of our items being closed + // The MenuItem will handle emitting MenuItemSubmenuClosedEvent + // through its event listeners + break; + } + } + } + } + return std::nullopt; // Let other handlers (including MenuItem handlers) process it + } +}; + +Menu::Menu(void* native_menu) + : pimpl_( + std::make_unique(IdAllocator::Allocate(), static_cast(native_menu))) { + // Set callbacks to emit events + pimpl_->opened_callback_ = [this](MenuId id) { Emit(id); }; + pimpl_->closed_callback_ = [this](MenuId id) { Emit(id); }; + + // Register window procedure handler for menu commands and events + HWND host_window = WindowMessageDispatcher::GetInstance().GetHostWindow(); + if (host_window) { + pimpl_->window_proc_handle_id_ = WindowMessageDispatcher::GetInstance().RegisterHandler( + host_window, [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return pimpl_->HandleWindowProc(hwnd, message, wparam, lparam); + }); + } +} + +Menu::Menu() : pimpl_(std::make_unique(IdAllocator::Allocate(), CreatePopupMenu())) { + // Set callbacks to emit events + pimpl_->opened_callback_ = [this](MenuId id) { Emit(id); }; + pimpl_->closed_callback_ = [this](MenuId id) { Emit(id); }; + + // Register window procedure handler for menu commands and events + HWND host_window = WindowMessageDispatcher::GetInstance().GetHostWindow(); + if (host_window) { + pimpl_->window_proc_handle_id_ = WindowMessageDispatcher::GetInstance().RegisterHandler( + host_window, [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return pimpl_->HandleWindowProc(hwnd, message, wparam, lparam); + }); + } +} + +Menu::~Menu() {} + +MenuId Menu::GetId() const { + return pimpl_->id_; +} + +void Menu::AddItem(std::shared_ptr item) { + if (!item) + return; + + pimpl_->items_.push_back(item); + + UINT flags = MF_STRING; + if (item->GetType() == MenuItemType::Separator) { + flags = MF_SEPARATOR; + } else if (item->GetType() == MenuItemType::Checkbox) { + flags |= (item->GetState() == MenuItemState::Checked) ? MF_CHECKED + : MF_UNCHECKED; + } else if (item->GetType() == MenuItemType::Radio) { + flags |= (item->GetState() == MenuItemState::Checked) ? MF_CHECKED + : MF_UNCHECKED; + } + + if (!item->IsEnabled()) { + flags |= MF_GRAYED; + } + + UINT_PTR menu_id = item->GetId(); + HMENU sub_menu = nullptr; + if (item->GetSubmenu()) { + sub_menu = static_cast(item->GetSubmenu()->GetNativeObject()); + flags |= MF_POPUP; + menu_id = reinterpret_cast(sub_menu); + } + + auto label_opt = item->GetLabel(); + std::string label_str = label_opt.has_value() ? *label_opt : ""; + std::wstring w_label_str = StringToWString(label_str); + AppendMenuW(pimpl_->hmenu_, flags, menu_id, w_label_str.c_str()); + + // Update the item's impl with menu info + item->pimpl_->parent_menu_ = pimpl_->hmenu_; +} + +void Menu::InsertItem(size_t index, std::shared_ptr item) { + if (!item) + return; + + if (index >= pimpl_->items_.size()) { + AddItem(item); + return; + } + + pimpl_->items_.insert(pimpl_->items_.begin() + index, item); + + UINT flags = MF_STRING | MF_BYPOSITION; + if (item->GetType() == MenuItemType::Separator) { + flags = MF_SEPARATOR | MF_BYPOSITION; + } else if (item->GetType() == MenuItemType::Checkbox) { + flags |= (item->GetState() == MenuItemState::Checked) ? MF_CHECKED + : MF_UNCHECKED; + } else if (item->GetType() == MenuItemType::Radio) { + flags |= (item->GetState() == MenuItemState::Checked) ? MF_CHECKED + : MF_UNCHECKED; + } + + if (!item->IsEnabled()) { + flags |= MF_GRAYED; + } + + UINT_PTR menu_id = item->GetId(); + if (item->GetSubmenu()) { + auto sub_menu = + static_cast(item->GetSubmenu()->GetNativeObject()); + flags |= MF_POPUP; + menu_id = reinterpret_cast(sub_menu); + } + + auto label_opt = item->GetLabel(); + std::string label_str = label_opt.has_value() ? *label_opt : ""; + std::wstring w_label_str = StringToWString(label_str); + InsertMenuW(pimpl_->hmenu_, static_cast(index), flags, menu_id, + w_label_str.c_str()); + + item->pimpl_->parent_menu_ = pimpl_->hmenu_; +} + +bool Menu::RemoveItem(std::shared_ptr item) { + if (!item) + return false; + + auto it = std::find(pimpl_->items_.begin(), pimpl_->items_.end(), item); + if (it != pimpl_->items_.end()) { + RemoveMenu(pimpl_->hmenu_, item->GetId(), MF_BYCOMMAND); + pimpl_->items_.erase(it); + return true; + } + return false; +} + +bool Menu::RemoveItemById(MenuItemId item_id) { + for (auto it = pimpl_->items_.begin(); it != pimpl_->items_.end(); ++it) { + if ((*it)->GetId() == item_id) { + RemoveMenu(pimpl_->hmenu_, item_id, MF_BYCOMMAND); + pimpl_->items_.erase(it); + return true; + } + } + return false; +} + +bool Menu::RemoveItemAt(size_t index) { + if (index >= pimpl_->items_.size()) + return false; + + RemoveMenu(pimpl_->hmenu_, static_cast(index), MF_BYPOSITION); + pimpl_->items_.erase(pimpl_->items_.begin() + index); + return true; +} + +void Menu::Clear() { + while (GetMenuItemCount(pimpl_->hmenu_) > 0) { + RemoveMenu(pimpl_->hmenu_, 0, MF_BYPOSITION); + } + pimpl_->items_.clear(); +} + +void Menu::AddSeparator() { + auto separator = std::make_shared("", MenuItemType::Separator); + AddItem(separator); +} + +void Menu::InsertSeparator(size_t index) { + auto separator = std::make_shared("", MenuItemType::Separator); + InsertItem(index, separator); +} + +size_t Menu::GetItemCount() const { + return pimpl_->items_.size(); +} + +std::shared_ptr Menu::GetItemAt(size_t index) const { + if (index >= pimpl_->items_.size()) + return nullptr; + return pimpl_->items_[index]; +} + +std::shared_ptr Menu::GetItemById(MenuItemId item_id) const { + for (const auto& item : pimpl_->items_) { + if (item->GetId() == item_id) { + return item; + } + } + return nullptr; +} + +std::vector> Menu::GetAllItems() const { + return pimpl_->items_; +} + +bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { + POINT pt = {0, 0}; + + // Determine position based on strategy type + switch (strategy.GetType()) { + case PositioningStrategy::Type::Absolute: + pt.x = static_cast(strategy.GetAbsolutePosition().x); + pt.y = static_cast(strategy.GetAbsolutePosition().y); + break; + + case PositioningStrategy::Type::CursorPosition: { + GetCursorPos(&pt); + break; + } + + case PositioningStrategy::Type::Relative: { + Rectangle rect = strategy.GetRelativeRectangle(); + Point offset = strategy.GetRelativeOffset(); + if (strategy.GetRelativeWindow() != nullptr) { + // rect and offset are in logical pixels (DIP) for Window-relative + HWND rel_hwnd = static_cast(strategy.GetRelativeWindow()->GetNativeObject()); + double scale = GetScaleFactorForWindow(rel_hwnd); + if (scale <= 0.0) + scale = 1.0; + pt.x = static_cast(std::lround((rect.x + offset.x) * scale)); + pt.y = static_cast(std::lround((rect.y + offset.y) * scale)); + } else { + // For plain rectangles, assume inputs already in screen pixels + pt.x = static_cast(rect.x + offset.x); + pt.y = static_cast(rect.y + offset.y); + } + break; + } + } + + // Get the host window for menus + HWND host_window = WindowMessageDispatcher::GetInstance().GetHostWindow(); + if (!host_window) { + return false; + } + + // Set the host window as foreground to ensure menu can be displayed + SetForegroundWindow(host_window); + + // Determine alignment flags based on placement (both axes) + // Horizontal: TPM_LEFTALIGN | TPM_CENTERALIGN | TPM_RIGHTALIGN + // Vertical: TPM_TOPALIGN | TPM_VCENTERALIGN | TPM_BOTTOMALIGN + UINT uFlags = 0; + switch (placement) { + case Placement::Top: + uFlags = TPM_BOTTOMALIGN | TPM_CENTERALIGN; // above anchor, horizontally centered + break; + case Placement::TopStart: + uFlags = TPM_BOTTOMALIGN | TPM_LEFTALIGN; // above anchor, align left + break; + case Placement::TopEnd: + uFlags = TPM_BOTTOMALIGN | TPM_RIGHTALIGN; // above anchor, align right + break; + case Placement::Right: + uFlags = TPM_LEFTALIGN | TPM_VCENTERALIGN; // right of anchor, vertically centered + break; + case Placement::RightStart: + uFlags = TPM_LEFTALIGN | TPM_TOPALIGN; // right of anchor, align top + break; + case Placement::RightEnd: + uFlags = TPM_LEFTALIGN | TPM_BOTTOMALIGN; // right of anchor, align bottom + break; + case Placement::Bottom: + uFlags = TPM_TOPALIGN | TPM_CENTERALIGN; // below anchor, horizontally centered + break; + case Placement::BottomStart: + uFlags = TPM_TOPALIGN | TPM_LEFTALIGN; // below anchor, align left + break; + case Placement::BottomEnd: + uFlags = TPM_TOPALIGN | TPM_RIGHTALIGN; // below anchor, align right + break; + case Placement::Left: + uFlags = TPM_RIGHTALIGN | TPM_VCENTERALIGN; // left of anchor, vertically centered + break; + case Placement::LeftStart: + uFlags = TPM_RIGHTALIGN | TPM_TOPALIGN; // left of anchor, align top + break; + case Placement::LeftEnd: + uFlags = TPM_RIGHTALIGN | TPM_BOTTOMALIGN; // left of anchor, align bottom + break; + } + + // Show the context menu using the host window + // Note: TrackPopupMenu is a blocking call + // - WM_INITMENUPOPUP is sent when the menu opens (triggers MenuOpenedEvent) + // - WM_UNINITMENUPOPUP is sent when the menu closes (triggers + // MenuClosedEvent) + TrackPopupMenu(pimpl_->hmenu_, uFlags, pt.x, pt.y, 0, host_window, nullptr); + + return true; +} + +bool Menu::Close() { + // Send WM_CANCELMODE to close any open menus + HWND host_window = WindowMessageDispatcher::GetInstance().GetHostWindow(); + if (host_window) { + // This will close the menu and trigger WM_UNINITMENUPOPUP + SendMessage(host_window, WM_CANCELMODE, 0, 0); + return true; + } + return false; +} + +void* Menu::GetNativeObjectInternal() const { + return pimpl_->hmenu_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/message_dialog_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/message_dialog_windows.cpp new file mode 100644 index 0000000..491f492 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/message_dialog_windows.cpp @@ -0,0 +1,104 @@ +// clang-format off +#include +#include +// clang-format on + +// Undefine Windows API macros that conflict with our method names +#ifdef GetMessage +#undef GetMessage +#endif + +#include +#include + +#include "../../dialog.h" +#include "../../message_dialog.h" +#include "string_utils_windows.h" + +namespace nativeapi { + +// Private implementation class for MessageDialog using Win32 MessageBox +class MessageDialog::Impl { + public: + Impl(const std::string& title, const std::string& message) : title_(title), message_(message) {} + + ~Impl() = default; + + void SetTitle(const std::string& title) { title_ = title; } + + std::string GetTitle() const { return title_; } + + void SetMessage(const std::string& message) { message_ = message; } + + std::string GetMessage() const { return message_; } + + bool Open(DialogModality modality) { + std::wstring wtitle = StringToWString(title_); + std::wstring wmessage = StringToWString(message_); + + UINT uType = MB_OK | MB_ICONINFORMATION; + + // Set modality + if (modality == DialogModality::Application) { + uType |= MB_APPLMODAL; + } else if (modality == DialogModality::Window) { + uType |= MB_SYSTEMMODAL; // Use system modal as approximation + } + + int result = MessageBoxW(nullptr, wmessage.c_str(), wtitle.c_str(), uType); + return result != 0; + } + + bool Close() { + // MessageBox doesn't support programmatic closing + return false; + } + + private: + std::string title_; + std::string message_; +}; + +// MessageDialog implementation +MessageDialog::MessageDialog(const std::string& title, const std::string& message) + : pimpl_(std::make_unique(title, message)) { + // Set default modality to None (non-modal) + SetModality(DialogModality::None); +} + +MessageDialog::~MessageDialog() = default; + +void MessageDialog::SetTitle(const std::string& title) { + pimpl_->SetTitle(title); +} + +std::string MessageDialog::GetTitle() const { + return pimpl_->GetTitle(); +} + +void MessageDialog::SetMessage(const std::string& message) { + pimpl_->SetMessage(message); +} + +std::string MessageDialog::GetMessage() const { + return pimpl_->GetMessage(); +} + +DialogModality MessageDialog::GetModality() const { + return modality_; +} + +void MessageDialog::SetModality(DialogModality modality) { + modality_ = modality; +} + +bool MessageDialog::Open() { + DialogModality modality = GetModality(); + return pimpl_->Open(modality); +} + +bool MessageDialog::Close() { + return pimpl_->Close(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/preferences_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/preferences_windows.cpp new file mode 100644 index 0000000..89ae7c2 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/preferences_windows.cpp @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include "../../preferences.h" + +namespace nativeapi { + +class Preferences::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Create registry key path + registry_path_ = "Software\\NativeAPI\\Preferences\\" + scope; + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + HKEY hkey; + LONG result = RegCreateKeyExA(HKEY_CURRENT_USER, registry_path_.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL); + + if (result != ERROR_SUCCESS) { + return false; + } + + result = + RegSetValueExA(hkey, key.c_str(), 0, REG_SZ, reinterpret_cast(value.c_str()), + static_cast(value.length() + 1)); + + RegCloseKey(hkey); + return result == ERROR_SUCCESS; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + HKEY hkey; + LONG result = RegOpenKeyExA(HKEY_CURRENT_USER, registry_path_.c_str(), 0, KEY_READ, &hkey); + + if (result != ERROR_SUCCESS) { + return default_value; + } + + char buffer[4096]; + DWORD buffer_size = sizeof(buffer); + DWORD type; + + result = RegQueryValueExA(hkey, key.c_str(), NULL, &type, reinterpret_cast(buffer), + &buffer_size); + + RegCloseKey(hkey); + + if (result == ERROR_SUCCESS && type == REG_SZ) { + return std::string(buffer); + } + + return default_value; + } + + bool Remove(const std::string& key) { + HKEY hkey; + LONG result = RegOpenKeyExA(HKEY_CURRENT_USER, registry_path_.c_str(), 0, KEY_WRITE, &hkey); + + if (result != ERROR_SUCCESS) { + return false; + } + + result = RegDeleteValueA(hkey, key.c_str()); + RegCloseKey(hkey); + + return result == ERROR_SUCCESS; + } + + bool Clear() { + // Delete the entire registry key + LONG result = RegDeleteTreeA(HKEY_CURRENT_USER, registry_path_.c_str()); + + // Recreate empty key + if (result == ERROR_SUCCESS) { + HKEY hkey; + RegCreateKeyExA(HKEY_CURRENT_USER, registry_path_.c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, + KEY_WRITE, NULL, &hkey, NULL); + RegCloseKey(hkey); + } + + return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND; + } + + bool Contains(const std::string& key) const { + HKEY hkey; + LONG result = RegOpenKeyExA(HKEY_CURRENT_USER, registry_path_.c_str(), 0, KEY_READ, &hkey); + + if (result != ERROR_SUCCESS) { + return false; + } + + result = RegQueryValueExA(hkey, key.c_str(), NULL, NULL, NULL, NULL); + RegCloseKey(hkey); + + return result == ERROR_SUCCESS; + } + + std::vector GetKeys() const { + std::vector keys; + + HKEY hkey; + LONG result = RegOpenKeyExA(HKEY_CURRENT_USER, registry_path_.c_str(), 0, KEY_READ, &hkey); + + if (result != ERROR_SUCCESS) { + return keys; + } + + DWORD index = 0; + char value_name[256]; + DWORD value_name_size; + + while (true) { + value_name_size = sizeof(value_name); + result = RegEnumValueA(hkey, index, value_name, &value_name_size, NULL, NULL, NULL, NULL); + + if (result == ERROR_NO_MORE_ITEMS) { + break; + } + + if (result == ERROR_SUCCESS) { + keys.push_back(std::string(value_name)); + } + + index++; + } + + RegCloseKey(hkey); + return keys; + } + + size_t GetSize() const { return GetKeys().size(); } + + std::map GetAll() const { + std::map result; + auto keys = GetKeys(); + + for (const auto& key : keys) { + result[key] = Get(key, ""); + } + + return result; + } + + const std::string& GetScope() const { return scope_; } + + private: + std::string scope_; + std::string registry_path_; +}; + +// Constructor implementations +Preferences::Preferences() : Preferences("default") {} + +Preferences::Preferences(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +Preferences::~Preferences() = default; + +// Interface implementation +bool Preferences::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string Preferences::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool Preferences::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool Preferences::Clear() { + return pimpl_->Clear(); +} + +bool Preferences::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector Preferences::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t Preferences::GetSize() const { + return pimpl_->GetSize(); +} + +std::map Preferences::GetAll() const { + return pimpl_->GetAll(); +} + +std::string Preferences::GetScope() const { + return pimpl_->GetScope(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/secure_storage_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/secure_storage_windows.cpp new file mode 100644 index 0000000..dffffc6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/secure_storage_windows.cpp @@ -0,0 +1,107 @@ +#include "../../secure_storage.h" + +namespace nativeapi { + +class SecureStorage::Impl { + public: + explicit Impl(const std::string& scope) : scope_(scope) { + // Stub implementation - no initialization + } + + ~Impl() = default; + + bool Set(const std::string& key, const std::string& value) { + // Stub implementation + return false; + } + + std::string Get(const std::string& key, const std::string& default_value) const { + // Stub implementation + return default_value; + } + + bool Remove(const std::string& key) { + // Stub implementation + return false; + } + + bool Clear() { + // Stub implementation + return false; + } + + bool Contains(const std::string& key) const { + // Stub implementation + return false; + } + + std::vector GetKeys() const { + // Stub implementation + return {}; + } + + size_t GetSize() const { + // Stub implementation + return 0; + } + + std::map GetAll() const { + // Stub implementation + return {}; + } + + std::string GetScope() const { return scope_; } + + private: + std::string scope_; +}; + +// Constructor implementations +SecureStorage::SecureStorage() : SecureStorage("default") {} + +SecureStorage::SecureStorage(const std::string& scope) : pimpl_(std::make_unique(scope)) {} + +SecureStorage::~SecureStorage() = default; + +bool SecureStorage::Set(const std::string& key, const std::string& value) { + return pimpl_->Set(key, value); +} + +std::string SecureStorage::Get(const std::string& key, const std::string& default_value) const { + return pimpl_->Get(key, default_value); +} + +bool SecureStorage::Remove(const std::string& key) { + return pimpl_->Remove(key); +} + +bool SecureStorage::Clear() { + return pimpl_->Clear(); +} + +bool SecureStorage::Contains(const std::string& key) const { + return pimpl_->Contains(key); +} + +std::vector SecureStorage::GetKeys() const { + return pimpl_->GetKeys(); +} + +size_t SecureStorage::GetSize() const { + return pimpl_->GetSize(); +} + +std::map SecureStorage::GetAll() const { + return pimpl_->GetAll(); +} + +std::string SecureStorage::GetScope() const { + return pimpl_->GetScope(); +} + +bool SecureStorage::IsAvailable() { + // Stub implementation - report as unavailable + return false; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/shortcut_manager_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/shortcut_manager_windows.cpp new file mode 100644 index 0000000..dfeaee1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/shortcut_manager_windows.cpp @@ -0,0 +1,383 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../../shortcut_manager.h" + +namespace nativeapi { +namespace { + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + return value; +} + +std::vector SplitAccelerator(const std::string& accelerator) { + std::vector parts; + std::string current; + for (char ch : accelerator) { + if (ch == '+') { + if (!current.empty()) { + parts.push_back(current); + current.clear(); + } + } else if (!std::isspace(static_cast(ch))) { + current.push_back(ch); + } + } + if (!current.empty()) { + parts.push_back(current); + } + return parts; +} + +bool ParseAcceleratorTokens(const std::string& accelerator, + std::vector& modifiers, + std::string& key_token) { + modifiers.clear(); + key_token.clear(); + + auto parts = SplitAccelerator(accelerator); + if (parts.empty()) { + return false; + } + + for (auto& part : parts) { + std::string token = ToLower(part); + if (token == "ctrl" || token == "control" || token == "alt" || token == "option" || + token == "shift" || token == "cmd" || token == "command" || token == "super" || + token == "meta" || token == "cmdorctrl" || token == "commandorcontrol") { + modifiers.push_back(token); + } else { + if (!key_token.empty()) { + return false; + } + key_token = token; + } + } + + return !key_token.empty(); +} + + +// Token -> Windows virtual-key code. +// +// Mirrors the token set in src/shortcut_manager.cpp's validator and the tables +// in the macOS/Linux backends, so the same accelerator string means the same +// key on every platform. +bool LookupWindowsKeyCode(const std::string& token, UINT& vk) { + static const std::unordered_map kKeyCodes = { + // Whitespace and editing. + {"space", VK_SPACE}, + {"tab", VK_TAB}, + {"enter", VK_RETURN}, + {"return", VK_RETURN}, + {"escape", VK_ESCAPE}, + {"esc", VK_ESCAPE}, + {"backspace", VK_BACK}, + {"delete", VK_DELETE}, + {"forwarddelete", VK_DELETE}, + {"insert", VK_INSERT}, + {"help", VK_HELP}, + + // Navigation. + {"home", VK_HOME}, + {"end", VK_END}, + {"pageup", VK_PRIOR}, + {"pagedown", VK_NEXT}, + {"up", VK_UP}, + {"down", VK_DOWN}, + {"left", VK_LEFT}, + {"right", VK_RIGHT}, + + // Punctuation, by name and by literal character. + {"plus", VK_OEM_PLUS}, + {"equal", VK_OEM_PLUS}, {"=", VK_OEM_PLUS}, + {"minus", VK_OEM_MINUS}, {"-", VK_OEM_MINUS}, + {"comma", VK_OEM_COMMA}, {",", VK_OEM_COMMA}, + {"period", VK_OEM_PERIOD}, {".", VK_OEM_PERIOD}, + {"slash", VK_OEM_2}, {"/", VK_OEM_2}, + {"backslash", VK_OEM_5}, {"\\", VK_OEM_5}, + {"semicolon", VK_OEM_1}, {";", VK_OEM_1}, + {"quote", VK_OEM_7}, {"'", VK_OEM_7}, + {"leftbracket", VK_OEM_4}, {"[", VK_OEM_4}, + {"rightbracket", VK_OEM_6}, {"]", VK_OEM_6}, + {"grave", VK_OEM_3}, {"backquote", VK_OEM_3}, {"`", VK_OEM_3}, + + // Keypad. + {"num0", VK_NUMPAD0}, {"num1", VK_NUMPAD1}, {"num2", VK_NUMPAD2}, + {"num3", VK_NUMPAD3}, {"num4", VK_NUMPAD4}, {"num5", VK_NUMPAD5}, + {"num6", VK_NUMPAD6}, {"num7", VK_NUMPAD7}, {"num8", VK_NUMPAD8}, + {"num9", VK_NUMPAD9}, + {"numdec", VK_DECIMAL}, + {"numadd", VK_ADD}, + {"numsub", VK_SUBTRACT}, + {"nummult", VK_MULTIPLY}, + {"numdiv", VK_DIVIDE}, + // Windows has no separate numpad-Enter virtual key; it reports VK_RETURN. + {"numenter", VK_RETURN}, + }; + + // Letters and digits map to their ASCII value as a virtual-key code. + if (token.size() == 1) { + unsigned char ch = static_cast(token[0]); + if (std::isalpha(ch)) { + vk = static_cast(std::toupper(ch)); + return true; + } + if (std::isdigit(ch)) { + vk = static_cast(ch); + return true; + } + } + + // Function keys. VK_F1..VK_F24 are contiguous, unlike the Carbon equivalents. + if (token.size() > 1 && token[0] == 'f' && + token.find_first_not_of("0123456789", 1) == std::string::npos) { + int fnum = std::stoi(token.substr(1)); + if (fnum >= 1 && fnum <= 24) { + vk = VK_F1 + (fnum - 1); + return true; + } + return false; + } + + auto it = kKeyCodes.find(token); + if (it == kKeyCodes.end()) { + return false; + } + vk = it->second; + return true; +} + +bool ParseAcceleratorWindows(const std::string& accelerator, UINT& modifiers, UINT& vk) { + modifiers = 0; + vk = 0; + + std::vector modifier_tokens; + std::string key_token; + if (!ParseAcceleratorTokens(accelerator, modifier_tokens, key_token)) { + return false; + } + + for (const auto& token : modifier_tokens) { + if (token == "ctrl" || token == "control" || token == "cmdorctrl" || + token == "commandorcontrol") { + modifiers |= MOD_CONTROL; + } else if (token == "alt" || token == "option") { + modifiers |= MOD_ALT; + } else if (token == "shift") { + modifiers |= MOD_SHIFT; + } else if (token == "cmd" || token == "command" || token == "super" || + token == "meta") { + modifiers |= MOD_WIN; + } + } + + modifiers |= MOD_NOREPEAT; + + return LookupWindowsKeyCode(key_token, vk); +} + + +} // namespace + +class ShortcutManagerImpl final : public ShortcutManager::Impl { + public: + explicit ShortcutManagerImpl(ShortcutManager* manager) : manager_(manager), running_(false) {} + + ~ShortcutManagerImpl() override { StopThread(); } + + bool IsSupported() override { return true; } + + bool RegisterShortcut(const std::shared_ptr& shortcut) override { + EnsureThread(); + + UINT modifiers = 0; + UINT vk = 0; + if (!ParseAcceleratorWindows(shortcut->GetAccelerator(), modifiers, vk)) { + return false; + } + + int hotkey_id = 0; + { + std::lock_guard lock(mutex_); + hotkey_id = next_hotkey_id_++; + } + if (!RegisterHotKey(hwnd_, hotkey_id, modifiers, vk)) { + return false; + } + + { + std::lock_guard lock(mutex_); + shortcut_to_hotkey_[shortcut->GetId()] = hotkey_id; + hotkey_to_shortcut_[hotkey_id] = shortcut->GetId(); + } + + return true; + } + + bool UnregisterShortcut(const std::shared_ptr& shortcut) override { + int hotkey_id = 0; + { + std::lock_guard lock(mutex_); + auto it = shortcut_to_hotkey_.find(shortcut->GetId()); + if (it == shortcut_to_hotkey_.end()) { + return false; + } + hotkey_id = it->second; + shortcut_to_hotkey_.erase(it); + hotkey_to_shortcut_.erase(hotkey_id); + } + + UnregisterHotKey(hwnd_, hotkey_id); + return true; + } + + void SetupEventMonitoring() override { EnsureThread(); } + + void CleanupEventMonitoring() override { + // Keep thread alive while shortcuts might still be registered. + } + + private: + static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + if (msg == WM_NCCREATE) { + auto* create_struct = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, GWLP_USERDATA, + reinterpret_cast(create_struct->lpCreateParams)); + return DefWindowProc(hwnd, msg, wparam, lparam); + } + + auto* self = + reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (!self) { + return DefWindowProc(hwnd, msg, wparam, lparam); + } + + switch (msg) { + case WM_HOTKEY: + self->HandleHotKey(static_cast(wparam)); + return 0; + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + return DefWindowProc(hwnd, msg, wparam, lparam); + } + } + + void HandleHotKey(int hotkey_id) { + ShortcutId shortcut_id = 0; + { + std::lock_guard lock(mutex_); + auto it = hotkey_to_shortcut_.find(hotkey_id); + if (it == hotkey_to_shortcut_.end()) { + return; + } + shortcut_id = it->second; + } + + auto shortcut = manager_->Get(shortcut_id); + if (!shortcut) { + return; + } + + if (!manager_->IsEnabled() || !shortcut->IsEnabled()) { + return; + } + + manager_->EmitShortcutActivated(shortcut_id, shortcut->GetAccelerator()); + shortcut->Invoke(); + } + + void EnsureThread() { + if (running_.load()) { + std::unique_lock lock(thread_mutex_); + thread_cv_.wait(lock, [this]() { return hwnd_ready_; }); + return; + } + + running_.store(true); + thread_ = std::thread([this]() { ThreadMain(); }); + + std::unique_lock lock(thread_mutex_); + thread_cv_.wait(lock, [this]() { return hwnd_ready_; }); + } + + void ThreadMain() { + const wchar_t* class_name = L"NativeApiShortcutManager"; + + WNDCLASSW wc = {}; + wc.lpfnWndProc = WindowProc; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = class_name; + RegisterClassW(&wc); + + HWND hwnd = CreateWindowExW(0, class_name, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, + wc.hInstance, this); + + { + std::lock_guard lock(thread_mutex_); + hwnd_ = hwnd; + hwnd_ready_ = true; + } + thread_cv_.notify_all(); + + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + void StopThread() { + if (!running_.load()) { + return; + } + + running_.store(false); + if (hwnd_) { + PostMessage(hwnd_, WM_CLOSE, 0, 0); + } + if (thread_.joinable()) { + thread_.join(); + } + hwnd_ = nullptr; + hwnd_ready_ = false; + } + + ShortcutManager* manager_; + std::mutex mutex_; + std::unordered_map shortcut_to_hotkey_; + std::unordered_map hotkey_to_shortcut_; + int next_hotkey_id_ = 1; + + std::atomic running_; + std::thread thread_; + std::mutex thread_mutex_; + std::condition_variable thread_cv_; + HWND hwnd_ = nullptr; + bool hwnd_ready_ = false; +}; + +ShortcutManager::ShortcutManager() + : pimpl_(std::make_unique(this)), next_shortcut_id_(1), enabled_(true) {} + +ShortcutManager::~ShortcutManager() { + UnregisterAll(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/string_utils_windows.h b/packages/cnativeapi/cxx_impl/src/platform/windows/string_utils_windows.h new file mode 100644 index 0000000..4bf5df3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/string_utils_windows.h @@ -0,0 +1,44 @@ +#ifndef NATIVEAPI_PLATFORM_WINDOWS_STRING_UTILS_H_ +#define NATIVEAPI_PLATFORM_WINDOWS_STRING_UTILS_H_ + +#include +#include + +namespace nativeapi { +namespace { // Anonymous namespace, visible only within the translation unit including this header + +// Convert std::string (UTF-8) to std::wstring (UTF-16) +inline std::wstring StringToWString(const std::string& str) { + if (str.empty()) + return std::wstring(); + + int size_needed = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring wstr(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), &wstr[0], size_needed); + return wstr; +} + +// Convert std::wstring (UTF-16) to std::string (UTF-8) +inline std::string WStringToString(const std::wstring& wstr) { + if (wstr.empty()) + return std::string(); + + int size_needed = + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); + std::string str(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), &str[0], size_needed, nullptr, + nullptr); + return str; +} + +// Convert WCHAR array to std::string +inline std::string WCharArrayToString(const WCHAR* wchar_array) { + if (!wchar_array) + return std::string(); + return WStringToString(std::wstring(wchar_array)); +} + +} // anonymous namespace +} // namespace nativeapi + +#endif // NATIVEAPI_PLATFORM_WINDOWS_STRING_UTILS_H_ diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/tray_icon_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/tray_icon_windows.cpp new file mode 100644 index 0000000..3271014 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/tray_icon_windows.cpp @@ -0,0 +1,403 @@ +// clang-format off +#include +#include +// clang-format on +#include +#include +#include +#include +#include + +#include "../../foundation/geometry.h" +#include "../../foundation/id_allocator.h" +#include "../../image.h" +#include "../../menu.h" +#include "../../positioning_strategy.h" +#include "../../tray_icon.h" +#include "string_utils_windows.h" +#include "window_message_dispatcher.h" + +namespace nativeapi { + +// Forward declaration for Windows-specific helper function from +// image_windows.cpp +HICON ImageToHICON(const Image* image, int width, int height); + +// Private implementation class +class TrayIcon::Impl { + public: + std::shared_ptr image_; + + // Callback function types + using ClickedCallback = std::function; + using RightClickedCallback = std::function; + using DoubleClickedCallback = std::function; + + Impl() + : hwnd_(nullptr), + icon_handle_(nullptr), + window_proc_handle_id_(-1), + event_monitoring_setup_(false), + context_menu_trigger_(ContextMenuTrigger::None) { + tray_icon_id_ = IdAllocator::Allocate(); + } + + Impl(HWND hwnd, + ClickedCallback clicked_callback, + RightClickedCallback right_clicked_callback, + DoubleClickedCallback double_clicked_callback) + : hwnd_(hwnd), + icon_handle_(nullptr), + window_proc_handle_id_(-1), + clicked_callback_(std::move(clicked_callback)), + right_clicked_callback_(std::move(right_clicked_callback)), + double_clicked_callback_(std::move(double_clicked_callback)), + event_monitoring_setup_(false), + context_menu_trigger_(ContextMenuTrigger::None) { + tray_icon_id_ = IdAllocator::Allocate(); + // Initialize NOTIFYICONDATA structure + ZeroMemory(&nid_, sizeof(NOTIFYICONDATAW)); + nid_.cbSize = sizeof(NOTIFYICONDATAW); + nid_.hWnd = hwnd_; + nid_.uID = static_cast(tray_icon_id_); // Use tray_icon_id_ directly + nid_.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; + nid_.uCallbackMessage = WM_USER + 1; // Custom message for tray icon events + + // Event monitoring will be set up when first listener is added + // via StartEventListening() override + } + + ~Impl() { + // Clean up event monitoring if it was set up + if (event_monitoring_setup_) { + CleanupEventMonitoring(); + } + + if (hwnd_) { + Shell_NotifyIconW(NIM_DELETE, &nid_); + // Note: We don't destroy the shared host window + } + if (icon_handle_) { + DestroyIcon(icon_handle_); + } + } + + // Handle window procedure delegate + std::optional HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_USER + 1 && wparam == static_cast(tray_icon_id_)) { + if (lparam == WM_LBUTTONUP) { + std::cout << "TrayIcon: Left button clicked, tray_icon_id = " << tray_icon_id_ << std::endl; + // Call clicked callback + if (clicked_callback_) { + clicked_callback_(tray_icon_id_); + } + } else if (lparam == WM_RBUTTONUP) { + std::cout << "TrayIcon: Right button clicked, tray_icon_id = " << tray_icon_id_ + << std::endl; + // Call right clicked callback + if (right_clicked_callback_) { + right_clicked_callback_(tray_icon_id_); + } + } else if (lparam == WM_LBUTTONDBLCLK) { + std::cout << "TrayIcon: Left button double-clicked, tray_icon_id = " << tray_icon_id_ + << std::endl; + // Call double clicked callback + if (double_clicked_callback_) { + double_clicked_callback_(tray_icon_id_); + } + } + return 0; + } + return std::nullopt; // Let default window procedure handle it + } + + void SetupEventMonitoring() { + if (event_monitoring_setup_) { + return; // Already set up + } + + if (!hwnd_) { + return; + } + + // Register window procedure handler + window_proc_handle_id_ = WindowMessageDispatcher::GetInstance().RegisterHandler( + hwnd_, [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return HandleWindowProc(hwnd, message, wparam, lparam); + }); + + event_monitoring_setup_ = true; + } + + void CleanupEventMonitoring() { + if (!event_monitoring_setup_) { + return; // Not set up + } + + // Unregister window procedure handler + if (window_proc_handle_id_ != -1) { + WindowMessageDispatcher::GetInstance().UnregisterHandler(window_proc_handle_id_); + window_proc_handle_id_ = -1; + } + + event_monitoring_setup_ = false; + } + + int window_proc_handle_id_; + HWND hwnd_; + NOTIFYICONDATAW nid_; + std::shared_ptr context_menu_; + HICON icon_handle_; + TrayIconId tray_icon_id_; + bool event_monitoring_setup_; + ContextMenuTrigger context_menu_trigger_; + + // Callback functions for event emission + ClickedCallback clicked_callback_; + RightClickedCallback right_clicked_callback_; + DoubleClickedCallback double_clicked_callback_; +}; + +TrayIcon::TrayIcon() : TrayIcon(nullptr) {} + +TrayIcon::TrayIcon(void* native_tray_icon) { + HWND hwnd = nullptr; + + if (native_tray_icon == nullptr) { + // Use the shared host window from WindowMessageDispatcher + hwnd = WindowMessageDispatcher::GetInstance().GetHostWindow(); + } else { + // Wrap existing native tray icon + // In a real implementation, you'd extract HWND from the tray parameter + // For now, this is mainly used by TrayManager for creating uninitialized + // icons + } + + // Initialize the Impl with the window handle + // The tray_icon_id will be allocated inside Impl constructor + if (hwnd) { + // Create callback functions that emit events + auto clicked_callback = [this](TrayIconId id) { + this->Emit(id); + // Auto-trigger context menu if configured + if (pimpl_ && pimpl_->context_menu_trigger_ == ContextMenuTrigger::Clicked) { + this->OpenContextMenu(); + } + }; + + auto right_clicked_callback = [this](TrayIconId id) { + this->Emit(id); + // Auto-trigger context menu if configured + if (pimpl_ && pimpl_->context_menu_trigger_ == ContextMenuTrigger::RightClicked) { + this->OpenContextMenu(); + } + }; + + auto double_clicked_callback = [this](TrayIconId id) { + this->Emit(id); + // Auto-trigger context menu if configured + if (pimpl_ && pimpl_->context_menu_trigger_ == ContextMenuTrigger::DoubleClicked) { + this->OpenContextMenu(); + } + }; + + pimpl_ = + std::make_unique(hwnd, std::move(clicked_callback), std::move(right_clicked_callback), + std::move(double_clicked_callback)); + } else { + // Failed to create window, create uninitialized Impl + pimpl_ = std::make_unique(); + } +} + +TrayIcon::~TrayIcon() {} + +void TrayIcon::StartEventListening() { + // Called automatically when first listener is added + // Set up platform event monitoring + pimpl_->SetupEventMonitoring(); +} + +void TrayIcon::StopEventListening() { + // Called automatically when last listener is removed + // Clean up platform event monitoring + pimpl_->CleanupEventMonitoring(); +} + +TrayIconId TrayIcon::GetId() { + return pimpl_->tray_icon_id_; +} + +void TrayIcon::SetIcon(std::shared_ptr image) { + if (!pimpl_->hwnd_) { + return; + } + + // Store the image reference + pimpl_->image_ = image; + + HICON hIcon = nullptr; + + if (image) { + // Get system tray icon size (following Windows guidelines) + int iconWidth = GetSystemMetrics(SM_CXSMICON); + int iconHeight = GetSystemMetrics(SM_CYSMICON); + + // Use the helper function to convert Image to HICON + // This handles file paths efficiently (like tray_manager_plugin.cpp) + // and falls back to bitmap conversion when needed + hIcon = ImageToHICON(image.get(), iconWidth, iconHeight); + + // Fallback to default icon if conversion failed + if (!hIcon) { + hIcon = LoadIcon(nullptr, IDI_APPLICATION); + } + } else { + // Use default application icon when no image is provided + hIcon = LoadIcon(nullptr, IDI_APPLICATION); + } + + if (hIcon) { + // Clean up previous icon + if (pimpl_->icon_handle_) { + DestroyIcon(pimpl_->icon_handle_); + } + + pimpl_->icon_handle_ = hIcon; + pimpl_->nid_.hIcon = hIcon; + + // Update the icon if it's currently visible + if (IsVisible()) { + Shell_NotifyIconW(NIM_MODIFY, &pimpl_->nid_); + } + } +} + +std::shared_ptr TrayIcon::GetIcon() const { + return pimpl_->image_; +} + +void TrayIcon::SetTitle(std::optional title) { + (void)title; // Unused on Windows + // Windows tray icons don't support title +} + +std::optional TrayIcon::GetTitle() { + // Windows tray icons don't support title + return std::nullopt; +} + +void TrayIcon::SetTooltip(std::optional tooltip) { + if (pimpl_->hwnd_) { + std::string tooltip_str = tooltip.has_value() ? *tooltip : ""; + std::wstring wtooltip = StringToWString(tooltip_str); + wcsncpy_s(pimpl_->nid_.szTip, _countof(pimpl_->nid_.szTip), wtooltip.c_str(), _TRUNCATE); + + // Update if icon is visible (check if hIcon is set as indicator) + if (pimpl_->nid_.hIcon) { + Shell_NotifyIconW(NIM_MODIFY, &pimpl_->nid_); + } + } +} + +std::optional TrayIcon::GetTooltip() { + if (pimpl_->hwnd_ && pimpl_->nid_.szTip[0] != L'\0') { + return WCharArrayToString(pimpl_->nid_.szTip); + } + return std::nullopt; +} + +void TrayIcon::SetContextMenu(std::shared_ptr menu) { + pimpl_->context_menu_ = menu; +} + +std::shared_ptr TrayIcon::GetContextMenu() { + return pimpl_->context_menu_; +} + +Rectangle TrayIcon::GetBounds() { + Rectangle bounds = {0, 0, 0, 0}; + + if (pimpl_->hwnd_ && IsVisible()) { + RECT rect; + NOTIFYICONIDENTIFIER niid = {}; + niid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + niid.hWnd = pimpl_->hwnd_; + niid.uID = static_cast(pimpl_->tray_icon_id_); + + // Get the rectangle of the notification icon + if (Shell_NotifyIconGetRect(&niid, &rect) == S_OK) { + bounds.x = rect.left; + bounds.y = rect.top; + bounds.width = rect.right - rect.left; + bounds.height = rect.bottom - rect.top; + } + } + + return bounds; +} + +bool TrayIcon::SetVisible(bool visible) { + if (!pimpl_->hwnd_) { + return false; + } + + bool currently_visible = IsVisible(); + + if (visible && !currently_visible) { + // Show the tray icon + return Shell_NotifyIconW(NIM_ADD, &pimpl_->nid_) == TRUE; + } else if (!visible && currently_visible) { + // Hide the tray icon + return Shell_NotifyIconW(NIM_DELETE, &pimpl_->nid_) == TRUE; + } else { + // Already in the desired state + return true; + } +} + +bool TrayIcon::IsVisible() { + if (!pimpl_->hwnd_) { + return false; + } + + // Check if the tray icon is visible by querying its bounds + NOTIFYICONIDENTIFIER niid = {}; + niid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + niid.hWnd = pimpl_->hwnd_; + niid.uID = static_cast(pimpl_->tray_icon_id_); + + RECT rect; + return Shell_NotifyIconGetRect(&niid, &rect) == S_OK; +} + +bool TrayIcon::OpenContextMenu() { + if (!pimpl_->context_menu_) { + return false; + } + + return pimpl_->context_menu_->Open(PositioningStrategy::CursorPosition()); +} + +bool TrayIcon::CloseContextMenu() { + if (!pimpl_->context_menu_) { + return true; // No menu to close, consider success + } + + // Close the context menu + return pimpl_->context_menu_->Close(); +} + +void TrayIcon::SetContextMenuTrigger(ContextMenuTrigger trigger) { + pimpl_->context_menu_trigger_ = trigger; +} + +ContextMenuTrigger TrayIcon::GetContextMenuTrigger() { + return pimpl_->context_menu_trigger_; +} + +void* TrayIcon::GetNativeObjectInternal() const { + return reinterpret_cast(static_cast(pimpl_->tray_icon_id_)); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/tray_manager_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/tray_manager_windows.cpp new file mode 100644 index 0000000..10e875c --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/tray_manager_windows.cpp @@ -0,0 +1,55 @@ +#include +#include + +#include "../../tray_icon.h" +#include "../../tray_manager.h" + +namespace nativeapi { + +// Define the Impl class for Windows (empty for now, as Windows doesn't need +// platform-specific data) +class TrayManager::Impl { + public: + Impl() {} + ~Impl() {} +}; + +TrayManager::TrayManager() : pimpl_(std::make_unique()), next_tray_id_(1) {} + +TrayManager::~TrayManager() { + std::lock_guard lock(mutex_); + // Clean up all managed tray icons + for (auto& pair : trays_) { + auto tray = pair.second; + if (tray) { + // The TrayIcon destructor will handle cleanup of the tray icon + } + } + trays_.clear(); +} + +bool TrayManager::IsSupported() { + return true; // Windows always supports system tray +} + +std::shared_ptr TrayManager::Get(TrayIconId id) { + std::lock_guard lock(mutex_); + + auto it = trays_.find(id); + if (it != trays_.end()) { + return it->second; + } + return nullptr; +} + +std::vector> TrayManager::GetAll() { + std::lock_guard lock(mutex_); + + std::vector> trays; + for (const auto& pair : trays_) { + trays.push_back(pair.second); + } + return trays; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/url_opener_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/url_opener_windows.cpp new file mode 100644 index 0000000..924a267 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/url_opener_windows.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include + +#include "../../url_opener.h" + +namespace nativeapi { +namespace { + +class WindowsUrlOpenerImpl final : public UrlOpener::Impl { + public: + bool IsSupported() const override { return true; } + + UrlOpenResult Open(const std::string& url) const override { + HINSTANCE launch_result = + ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + const auto code = reinterpret_cast(launch_result); + if (code <= 32) { + std::ostringstream oss; + oss << "ShellExecute failed with code " << code << "."; + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvocationFailed; + result.error_message = oss.str(); + return result; + } + + UrlOpenResult result; + result.success = true; + result.error_code = UrlOpenErrorCode::kNone; + return result; + } +}; + +} // namespace + +UrlOpener::UrlOpener() : pimpl_(std::make_unique()) {} + +UrlOpener::~UrlOpener() = default; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/window_manager_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/window_manager_windows.cpp new file mode 100644 index 0000000..48439da --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/window_manager_windows.cpp @@ -0,0 +1,561 @@ +#include +#include +#include + +#include +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" +#include "string_utils_windows.h" + +#pragma comment(lib, "psapi.lib") + +namespace nativeapi { + +// Property name for storing window ID in HWND (must match window_windows.cpp) +static const wchar_t* kWindowIdProperty = L"NativeAPIWindowId"; + +// Helper function to get window ID from HWND +// First tries to read from custom property, then creates Window object if needed +static WindowId GetWindowIdFromHwnd(HWND hwnd) { + if (!hwnd) { + return IdAllocator::kInvalidId; + } + + // First, try to get window ID from HWND's custom property + HANDLE prop_handle = GetPropW(hwnd, kWindowIdProperty); + if (prop_handle) { + WindowId window_id = static_cast(reinterpret_cast(prop_handle)); + if (window_id != IdAllocator::kInvalidId && window_id != 0) { + return window_id; + } + } + + // If property doesn't exist, create a new Window object and register it + // Use shared_ptr so it can be properly registered in WindowRegistry + auto window = std::make_shared(hwnd); + WindowId window_id = window->GetId(); + + // Register the window manually since constructor's shared_from_this() fails + // during construction (shared_ptr control block not fully initialized yet) + if (window_id != IdAllocator::kInvalidId) { + WindowRegistry::GetInstance().Add(window_id, window); + } + + return window_id; +} + +namespace { + +using PFN_ShowWindow = BOOL(WINAPI*)(HWND, int); +using PFN_ShowWindowAsync = BOOL(WINAPI*)(HWND, int); + +static PFN_ShowWindow g_original_show_window = nullptr; +static PFN_ShowWindowAsync g_original_show_window_async = nullptr; +static bool g_hooks_installed = false; + +static bool IsShowCommand(int cmd) { + switch (cmd) { + case SW_SHOW: + case SW_SHOWNORMAL: + case SW_SHOWDEFAULT: + case SW_SHOWMAXIMIZED: + case SW_SHOWNOACTIVATE: + case SW_RESTORE: + return true; + default: + return false; + } +} + +// Intercept show/hide commands and invoke hooks if registered +// Returns true if hook handled the operation (skip original implementation) +static bool TryHandleWithHook(HWND hwnd, int cmd) { + WindowId window_id = GetWindowIdFromHwnd(hwnd); + if (window_id == IdAllocator::kInvalidId) { + return false; + } + + auto& manager = WindowManager::GetInstance(); + + if (cmd == SW_HIDE && manager.HasWillHideHook()) { + manager.HandleWillHide(window_id); + return true; + } + + if (IsShowCommand(cmd) && manager.HasWillShowHook()) { + manager.HandleWillShow(window_id); + return true; + } + + return false; +} + +static BOOL WINAPI HookedShowWindow(HWND hwnd, int nCmdShow) { + if (TryHandleWithHook(hwnd, nCmdShow)) { + return TRUE; + } + + if (g_original_show_window) { + return g_original_show_window(hwnd, nCmdShow); + } + + auto p = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"user32.dll"), "ShowWindow")); + return p ? p(hwnd, nCmdShow) : FALSE; +} + +static BOOL WINAPI HookedShowWindowAsync(HWND hwnd, int nCmdShow) { + if (TryHandleWithHook(hwnd, nCmdShow)) { + return TRUE; + } + + if (g_original_show_window_async) { + return g_original_show_window_async(hwnd, nCmdShow); + } + + auto p = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"user32.dll"), "ShowWindowAsync")); + return p ? p(hwnd, nCmdShow) : FALSE; +} + +static bool CaseInsensitiveEquals(const char* a, const char* b) { + if (!a || !b) + return false; + while (*a && *b) { + char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b; + if (ca != cb) + return false; + ++a; + ++b; + } + return *a == *b; +} + +static void PatchIATInModule(HMODULE module, + FARPROC target, + FARPROC replacement, + const char* func_name) { + if (!module) + return; + + auto base = reinterpret_cast(module); + auto dos = reinterpret_cast(base); + if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) + return; + + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (!nt || nt->Signature != IMAGE_NT_SIGNATURE) + return; + + auto& import_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (import_dir.VirtualAddress == 0) + return; + + auto import_desc = reinterpret_cast(base + import_dir.VirtualAddress); + for (; import_desc->Name != 0; ++import_desc) { + auto dll_name = reinterpret_cast(base + import_desc->Name); + // Only hook USER32.dll to reduce risk + if (!dll_name) + continue; + if (!(CaseInsensitiveEquals(dll_name, "user32.dll"))) + continue; + + auto orig_thunk = reinterpret_cast(base + import_desc->OriginalFirstThunk); + auto thunk = reinterpret_cast(base + import_desc->FirstThunk); + if (!orig_thunk || !thunk) + continue; + + for (; orig_thunk->u1.AddressOfData != 0; ++orig_thunk, ++thunk) { + if (IMAGE_SNAP_BY_ORDINAL(orig_thunk->u1.Ordinal)) { + continue; // Skip ordinals + } + auto import = reinterpret_cast(base + orig_thunk->u1.AddressOfData); + if (!import || !import->Name) + continue; + const char* name = reinterpret_cast(import->Name); + if (!CaseInsensitiveEquals(name, func_name)) + continue; + + // Change protection and write new function pointer + DWORD old_protect; + if (VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &old_protect)) { + // Store original (first time only) + (void)target; // target kept for symmetry; not used here + thunk->u1.Function = reinterpret_cast(replacement); + VirtualProtect(&thunk->u1.Function, sizeof(void*), old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), &thunk->u1.Function, sizeof(void*)); + } + } + } +} + +static void RestoreIATInModule(HMODULE module, FARPROC original, const char* func_name) { + if (!module || !original) + return; + + auto base = reinterpret_cast(module); + auto dos = reinterpret_cast(base); + if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) + return; + + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (!nt || nt->Signature != IMAGE_NT_SIGNATURE) + return; + + auto& import_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (import_dir.VirtualAddress == 0) + return; + + auto import_desc = reinterpret_cast(base + import_dir.VirtualAddress); + for (; import_desc->Name != 0; ++import_desc) { + auto dll_name = reinterpret_cast(base + import_desc->Name); + if (!dll_name) + continue; + if (!(CaseInsensitiveEquals(dll_name, "user32.dll"))) + continue; + + auto orig_thunk = reinterpret_cast(base + import_desc->OriginalFirstThunk); + auto thunk = reinterpret_cast(base + import_desc->FirstThunk); + if (!orig_thunk || !thunk) + continue; + + for (; orig_thunk->u1.AddressOfData != 0; ++orig_thunk, ++thunk) { + if (IMAGE_SNAP_BY_ORDINAL(orig_thunk->u1.Ordinal)) { + continue; + } + auto import = reinterpret_cast(base + orig_thunk->u1.AddressOfData); + if (!import || !import->Name) + continue; + const char* name = reinterpret_cast(import->Name); + if (!CaseInsensitiveEquals(name, func_name)) + continue; + + DWORD old_protect; + if (VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &old_protect)) { + thunk->u1.Function = reinterpret_cast(original); + VirtualProtect(&thunk->u1.Function, sizeof(void*), old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), &thunk->u1.Function, sizeof(void*)); + } + } + } +} + +static void ForEachProcessModule(std::function fn) { + HMODULE modules[1024]; + DWORD bytes_needed = 0; + if (!EnumProcessModules(GetCurrentProcess(), modules, sizeof(modules), &bytes_needed)) { + // Fallback: at least patch main module + fn(GetModuleHandle(nullptr)); + return; + } + size_t count = bytes_needed / sizeof(HMODULE); + for (size_t i = 0; i < count; ++i) { + fn(modules[i]); + } +} + +static void InstallHooks() { + if (g_hooks_installed) + return; + + HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (!user32) + user32 = LoadLibraryW(L"user32.dll"); + if (!user32) + return; + + g_original_show_window = reinterpret_cast(GetProcAddress(user32, "ShowWindow")); + g_original_show_window_async = + reinterpret_cast(GetProcAddress(user32, "ShowWindowAsync")); + if (!g_original_show_window) + return; + + ForEachProcessModule([](HMODULE m) { + PatchIATInModule(m, reinterpret_cast(g_original_show_window), + reinterpret_cast(HookedShowWindow), "ShowWindow"); + if (g_original_show_window_async) { + PatchIATInModule(m, reinterpret_cast(g_original_show_window_async), + reinterpret_cast(HookedShowWindowAsync), "ShowWindowAsync"); + } + }); + + g_hooks_installed = true; +} + +static void UninstallHooks() { + if (!g_hooks_installed) + return; + + ForEachProcessModule([](HMODULE m) { + if (g_original_show_window) { + RestoreIATInModule(m, reinterpret_cast(g_original_show_window), "ShowWindow"); + } + if (g_original_show_window_async) { + RestoreIATInModule(m, reinterpret_cast(g_original_show_window_async), + "ShowWindowAsync"); + } + }); + g_hooks_installed = false; +} + +} // namespace + +// Private implementation to hide Windows-specific details +class WindowManager::Impl { + public: + Impl(WindowManager* manager) : manager_(manager) {} + ~Impl() {} + + void StartEventListening() { + // Windows event monitoring would typically be done through: + // - SetWinEventHook for system-wide window events + // - Window subclassing for specific window events + // This is a placeholder implementation + } + + void StopEventListening() { + // Clean up any event hooks or monitoring + } + + void OnWindowEvent(HWND hwnd, const std::string& event_type) { + // Get window ID, first trying custom property, then creating Window if needed + WindowId window_id = GetWindowIdFromHwnd(hwnd); + if (window_id == IdAllocator::kInvalidId) { + return; + } + + if (event_type == "focused") { + WindowFocusedEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "blurred") { + WindowBlurredEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "minimized") { + WindowMinimizedEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "restored") { + WindowRestoredEvent event(window_id); + manager_->DispatchWindowEvent(event); + } else if (event_type == "resized") { + RECT rect; + GetWindowRect(hwnd, &rect); + Size new_size = {static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)}; + WindowResizedEvent event(window_id, new_size); + manager_->DispatchWindowEvent(event); + } else if (event_type == "moved") { + RECT rect; + GetWindowRect(hwnd, &rect); + Point new_position = {static_cast(rect.left), static_cast(rect.top)}; + WindowMovedEvent event(window_id, new_position); + manager_->DispatchWindowEvent(event); + } else if (event_type == "closing") { + // Window closing event - no longer emitted + } + } + + private: + WindowManager* manager_; + // Optional pre-show/hide/close hooks + std::optional will_show_hook_; + std::optional will_hide_hook_; + std::optional will_close_hook_; + + friend class WindowManager; +}; + +WindowManager::WindowManager() : pimpl_(std::make_unique(this)) { + StartEventListening(); +} + +WindowManager::~WindowManager() { + StopEventListening(); +} + +std::shared_ptr WindowManager::Get(WindowId id) { + // First try to get from registry + auto window = WindowRegistry::GetInstance().Get(id); + if (window) { + return window; + } + + // If not in registry, enumerate all windows to find it + // This will create and register the window if it exists + GetAll(); + + // Try again after enumeration + return WindowRegistry::GetInstance().Get(id); +} + +// Callback for EnumWindows to collect all top-level windows +static BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) { + auto* windows = reinterpret_cast*>(lParam); + + // Only include visible windows that are not minimized to taskbar + // and have a title (filters out many background windows) + if (IsWindowVisible(hwnd)) { + int length = GetWindowTextLengthW(hwnd); + if (length > 0) { + // Check if it's a normal window (not tool window, etc.) + LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + if (!(exStyle & WS_EX_TOOLWINDOW)) { + windows->push_back(hwnd); + } + } + } + + return TRUE; // Continue enumeration +} + +std::vector> WindowManager::GetAll() { + std::vector hwnds; + + // Enumerate all top-level windows + EnumWindows(EnumWindowsCallback, reinterpret_cast(&hwnds)); + + std::vector> windows; + windows.reserve(hwnds.size()); + + for (HWND hwnd : hwnds) { + // Get window ID from HWND, creating Window object if needed + WindowId window_id = GetWindowIdFromHwnd(hwnd); + + if (window_id != IdAllocator::kInvalidId) { + // Try to get existing window from registry + auto window = WindowRegistry::GetInstance().Get(window_id); + if (window) { + windows.push_back(window); + } + } + } + + return windows; +} + +std::shared_ptr WindowManager::GetCurrent() { + HWND hwnd = GetActiveWindow(); + if (hwnd) { + WindowId window_id = GetWindowIdFromHwnd(hwnd); + if (window_id != IdAllocator::kInvalidId) { + return Get(window_id); + } + } + return nullptr; +} + +void WindowManager::SetWillShowHook(std::optional hook) { + pimpl_->will_show_hook_ = std::move(hook); + + bool has_any_hook = pimpl_->will_show_hook_.has_value() || pimpl_->will_hide_hook_.has_value(); + has_any_hook ? InstallHooks() : UninstallHooks(); +} + +void WindowManager::SetWillHideHook(std::optional hook) { + pimpl_->will_hide_hook_ = std::move(hook); + + bool has_any_hook = pimpl_->will_show_hook_.has_value() || pimpl_->will_hide_hook_.has_value(); + has_any_hook ? InstallHooks() : UninstallHooks(); +} + +void WindowManager::SetWillCloseHook(std::optional hook) { + pimpl_->will_close_hook_ = std::move(hook); + // ponytail: Windows close-interceptie vereist een WH_CBT hook of window + // subclassing op WM_CLOSE — niet geïmplementeerd in deze iteratie. + // De hook wordt wel opgeslagen zodat HandleWillClose werkt zodra de + // event-monitoring dit signaal oppakt. +} + +bool WindowManager::HasWillShowHook() const { + return pimpl_->will_show_hook_.has_value(); +} + +bool WindowManager::HasWillHideHook() const { + return pimpl_->will_hide_hook_.has_value(); +} + +bool WindowManager::HasWillCloseHook() const { + return pimpl_->will_close_hook_.has_value(); +} + +void WindowManager::HandleWillShow(WindowId id) { + if (pimpl_->will_show_hook_) { + (*pimpl_->will_show_hook_)(id); + } +} + +void WindowManager::HandleWillHide(WindowId id) { + if (pimpl_->will_hide_hook_) { + (*pimpl_->will_hide_hook_)(id); + } +} + +void WindowManager::HandleWillClose(WindowId id) { + if (pimpl_->will_close_hook_) { + (*pimpl_->will_close_hook_)(id); + } +} + +bool WindowManager::CallOriginalShow(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + HWND hwnd = static_cast(native); + // On Windows, call the original ShowWindow through the function pointer + if (g_original_show_window) { + return g_original_show_window(hwnd, SW_SHOW) != FALSE; + } + return false; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + HWND hwnd = static_cast(native); + // On Windows, call the original ShowWindow through the function pointer + if (g_original_show_window) { + return g_original_show_window(hwnd, SW_HIDE) != FALSE; + } + return false; +} + +bool WindowManager::CallOriginalClose(WindowId id) { + auto window = Get(id); + if (!window) { + return false; + } + void* native = window->GetNativeObject(); + if (!native) { + return false; + } + HWND hwnd = static_cast(native); + // On Windows, send WM_CLOSE to trigger the normal close path + PostMessage(hwnd, WM_CLOSE, 0, 0); + return true; +} + +void WindowManager::StartEventListening() { + pimpl_->StartEventListening(); +} + +void WindowManager::StopEventListening() { + pimpl_->StopEventListening(); +} + +void WindowManager::DispatchWindowEvent(const WindowEvent& event) { + Emit(event); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.cpp new file mode 100644 index 0000000..563562b --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.cpp @@ -0,0 +1,239 @@ +#include "window_message_dispatcher.h" +#include +#include + +namespace nativeapi { + +WindowMessageDispatcher& WindowMessageDispatcher::GetInstance() { + // Use heap allocation to avoid static destruction order issues + // The instance is never destroyed to ensure it remains valid during + // the entire program lifetime, including during static destruction + static auto* instance = new WindowMessageDispatcher(); + return *instance; +} + +WindowMessageDispatcher::~WindowMessageDispatcher() { + std::lock_guard lock(mutex_); + + // Destroy host window if it exists + if (host_window_) { + DestroyWindow(host_window_); + host_window_ = nullptr; + } + + // Uninstall all hooks before destruction + for (const auto& [hwnd, _] : original_procs_) { + UninstallHook(hwnd); + } + original_procs_.clear(); +} + +int WindowMessageDispatcher::RegisterHandler(WindowMessageHandler handler) { + std::lock_guard lock(mutex_); + + int id = next_id_++; + handlers_[id] = {std::move(handler), HWND(0)}; // HWND(0) for global handler + return id; +} + +int WindowMessageDispatcher::RegisterHandler(HWND hwnd, WindowMessageHandler handler) { + // Check if hook needs to be installed (outside of lock to avoid deadlock) + bool needs_hook = false; + { + std::lock_guard lock(mutex_); + needs_hook = (original_procs_.find(hwnd) == original_procs_.end()); + } + + // Install hook if needed (outside of lock) + if (needs_hook) { + InstallHook(hwnd); + } + + // Register the handler (inside lock) + std::lock_guard lock(mutex_); + int id = next_id_++; + handlers_[id] = {std::move(handler), hwnd}; + + return id; +} + +bool WindowMessageDispatcher::UnregisterHandler(int id) { + HWND target_hwnd = HWND(0); + bool should_uninstall = false; + + { + std::lock_guard lock(mutex_); + + auto it = handlers_.find(id); + if (it == handlers_.end()) { + return false; + } + + target_hwnd = it->second.target_hwnd; + handlers_.erase(it); + + // Check if this was the last handler for this window + if (target_hwnd != HWND(0)) { + bool has_other_handlers = std::any_of( + handlers_.begin(), handlers_.end(), + [target_hwnd](const auto& pair) { return pair.second.target_hwnd == target_hwnd; }); + + should_uninstall = !has_other_handlers; + } + } + + // Uninstall hook if needed (outside of lock to avoid deadlock) + if (should_uninstall) { + UninstallHook(target_hwnd); + } + + return true; +} + +LRESULT CALLBACK WindowMessageDispatcher::DispatchWindowProc(HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam) { + auto& dispatcher = GetInstance(); + + // Get original window procedure and copy handlers while holding lock + WNDPROC original_proc = nullptr; + std::vector> handlers_vector; + + { + std::lock_guard lock(dispatcher.mutex_); + + // Get original window procedure + auto proc_it = dispatcher.original_procs_.find(hwnd); + if (proc_it == dispatcher.original_procs_.end()) { + return DefWindowProc(hwnd, msg, wparam, lparam); + } + + original_proc = proc_it->second; + + // Copy handlers while holding lock (to avoid deadlock when handlers call + // back) + handlers_vector.assign(dispatcher.handlers_.begin(), dispatcher.handlers_.end()); + } + + // Try handlers in reverse order (most recently registered first) + // Process handlers without holding the mutex to avoid deadlock + for (auto it = handlers_vector.rbegin(); it != handlers_vector.rend(); ++it) { + const auto& [id, entry] = *it; + + // Check if this handler applies to this window + if (entry.target_hwnd == HWND(0) || entry.target_hwnd == hwnd) { + auto result = entry.handler(hwnd, msg, wparam, lparam); + if (result.has_value()) { + return result.value(); + } + } + } + + // No handler consumed the message, call original procedure + return CallWindowProc(original_proc, hwnd, msg, wparam, lparam); +} + +bool WindowMessageDispatcher::InstallHook(HWND hwnd) { + if (!hwnd || !IsWindow(hwnd)) { + return false; + } + + // Get current window procedure + WNDPROC current_proc = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_WNDPROC)); + if (!current_proc) { + return false; + } + + // If the window already has DispatchWindowProc, don't install it again + if (current_proc == DispatchWindowProc) { + return true; // Already installed + } + + // Store original procedure + original_procs_[hwnd] = current_proc; + + // Install our dispatcher as the new window procedure + SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast(DispatchWindowProc)); + + return true; +} + +void WindowMessageDispatcher::UninstallHook(HWND hwnd) { + auto it = original_procs_.find(hwnd); + if (it == original_procs_.end()) { + return; + } + + WNDPROC original_proc = it->second; + + // Don't restore window procedure for host window - it should keep DispatchWindowProc + if (hwnd != host_window_) { + // Restore original window procedure + SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast(original_proc)); + } + + // Remove from our tracking + original_procs_.erase(it); +} + +HWND WindowMessageDispatcher::GetHostWindow() { + // Check if host window already exists (without holding lock to avoid deadlock) + if (host_window_ && IsWindow(host_window_)) { + return host_window_; + } + + // Create a hidden window class for hosting + static const wchar_t* class_name = L"NativeApiHostWindow"; + + WNDCLASSW wc = {}; + wc.lpfnWndProc = DispatchWindowProc; // Use dispatcher for host window + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = class_name; + + // Register the window class (only once) + static bool class_registered = false; + if (!class_registered) { + if (RegisterClassW(&wc)) { + class_registered = true; + } else { + return nullptr; + } + } + + // Create the hidden host window (outside of lock to avoid deadlock) + HWND new_host_window = CreateWindowExW(WS_EX_TOOLWINDOW, // Extended style: tool window + class_name, // Window class + L"NativeApi Host", // Window title + WS_OVERLAPPED, // Window style: overlapped window + 0, 0, // Position + 1, 1, // Size (minimal) + HWND_MESSAGE, // Parent: message-only window + nullptr, // Menu + GetModuleHandle(nullptr), // Instance + nullptr // Additional data + ); + + if (new_host_window) { + // Ensure the window is hidden + ShowWindow(new_host_window, SW_HIDE); + + // Now acquire lock to register the window + std::lock_guard lock(mutex_); + + // Double-check if another thread created the window while we were creating it + if (host_window_ && IsWindow(host_window_)) { + // Another thread won, destroy our window and use theirs + DestroyWindow(new_host_window); + return host_window_; + } + + // Register the host window in original_procs_ with DefWindowProc as fallback + host_window_ = new_host_window; + original_procs_[host_window_] = DefWindowProcW; + } + + return host_window_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.h b/packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.h new file mode 100644 index 0000000..58b1eb5 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/window_message_dispatcher.h @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace nativeapi { + +/** + * @brief Function type for handling Windows messages. + * + * @param hwnd Window handle receiving the message + * @param msg Message identifier (WM_* constants) + * @param wparam Message-specific parameter + * @param lparam Message-specific parameter + * @return std::optional If a value is returned, the message is + * considered handled. If std::nullopt is returned, the message continues to + * other handlers. + */ +using WindowMessageHandler = + std::function(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)>; + +/** + * @brief Singleton dispatcher for Windows message handling across multiple + * windows. + * + * This class provides a centralized way to register message handlers for + * Windows messages. It supports both global handlers (applied to all windows) + * and window-specific handlers. The dispatcher automatically hooks into window + * procedures using SetWindowLongPtr and restores original procedures when + * handlers are unregistered. + * + * Thread Safety: All public methods are thread-safe. + * + * Usage Example: + * ```cpp + * auto& dispatcher = WindowMessageDispatcher::GetInstance(); + * + * // Register global handler for all windows + * int global_id = dispatcher.RegisterHandler([](HWND hwnd, UINT msg, WPARAM wp, + * LPARAM lp) { if (msg == WM_SIZE) { + * // Handle window resize + * return std::make_optional(0); + * } + * return std::nullopt; // Let other handlers process + * }); + * + * // Register window-specific handler + * int window_id = dispatcher.RegisterHandler(specific_hwnd, [](HWND hwnd, UINT + * msg, WPARAM wp, LPARAM lp) { if (msg == WM_CLOSE) { + * // Prevent window from closing + * return std::make_optional(0); + * } + * return std::nullopt; + * }); + * + * // Unregister when done + * dispatcher.UnregisterHandler(global_id); + * dispatcher.UnregisterHandler(window_id); + * ``` + */ +class WindowMessageDispatcher { + public: + /** + * @brief Get the singleton instance of the dispatcher. + * @return Reference to the singleton WindowMessageDispatcher instance. + */ + static WindowMessageDispatcher& GetInstance(); + + /** + * @brief Register a global message handler that applies to all windows. + * + * @param handler Function to call for Windows messages + * @return int Handler ID for unregistration + */ + int RegisterHandler(WindowMessageHandler handler); + + /** + * @brief Register a message handler for a specific window. + * + * This method automatically installs a hook into the window's procedure + * if not already installed. The hook is removed when the last handler + * for the window is unregistered. + * + * @param hwnd Target window handle + * @param handler Function to call for Windows messages + * @return int Handler ID for unregistration + */ + int RegisterHandler(HWND hwnd, WindowMessageHandler handler); + + /** + * @brief Unregister a message handler by ID. + * + * @param id Handler ID returned from RegisterHandler + * @return bool true if handler was found and removed, false otherwise + */ + bool UnregisterHandler(int id); + + /** + * @brief Get a host window for tray icons and menus. + * + * This method provides a hidden window that can be used as a parent + * for tray icons and menus. The window is created once and reused. + * + * @return HWND Host window handle, or nullptr if creation failed + */ + HWND GetHostWindow(); + + /** + * @brief Internal window procedure function used for message dispatching. + * + * This function is installed as the window procedure for windows that have + * registered handlers. It processes messages through registered handlers + * and falls back to the original window procedure if no handler consumes + * the message. + * + * @param hwnd Window handle + * @param msg Message identifier + * @param wparam Message parameter + * @param lparam Message parameter + * @return LRESULT Message processing result + */ + static LRESULT CALLBACK DispatchWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + + private: + WindowMessageDispatcher() = default; + ~WindowMessageDispatcher(); + + /** + * @brief Entry for a registered message handler. + */ + struct HandlerEntry { + WindowMessageHandler handler; ///< The handler function + HWND target_hwnd; ///< Target window (HWND(0) for global handlers) + }; + + /** + * @brief Install message hook for a window. + * + * @param hwnd Window handle to hook + * @return bool true if hook was installed successfully + */ + bool InstallHook(HWND hwnd); + + /** + * @brief Uninstall message hook for a window. + * + * @param hwnd Window handle to unhook + */ + void UninstallHook(HWND hwnd); + + ///< Mutex for thread safety + mutable std::mutex mutex_; + ///< Registered handlers by ID + std::unordered_map handlers_; + ///< Original window procedures + std::unordered_map original_procs_; + ///< Next available handler ID + int next_id_ = 1; + ///< Host window for tray icons and menus + HWND host_window_ = nullptr; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/platform/windows/window_windows.cpp b/packages/cnativeapi/cxx_impl/src/platform/windows/window_windows.cpp new file mode 100644 index 0000000..1df6c62 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/windows/window_windows.cpp @@ -0,0 +1,983 @@ +#include +#include +#include +#include +#include "../../foundation/id_allocator.h" +#include "../../window.h" +#include "../../window_manager.h" +#include "../../window_registry.h" +#include "dpi_utils_windows.h" +#include "string_utils_windows.h" +#include "window_message_dispatcher.h" + +#pragma comment(lib, "dwmapi.lib") + +namespace nativeapi { + +// Property name for storing window ID in HWND +static const wchar_t* kWindowIdProperty = L"NativeAPIWindowId"; + +// Forward declaration +static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +// Private implementation class +class Window::Impl { + public: + Impl(HWND hwnd, WindowId id) + : hwnd_(hwnd), + window_id_(id), + title_bar_style_(TitleBarStyle::Normal), + visual_effect_(VisualEffect::None) {} + HWND hwnd_; + WindowId window_id_; + TitleBarStyle title_bar_style_; + VisualEffect visual_effect_; + Size min_size_{0, 0}; + Size max_size_{0, 0}; + int min_max_handler_id_ = 0; +}; + +// Custom window procedure to handle window messages +static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + switch (uMsg) { + case WM_WINDOWPOSCHANGING: { + // Intercept visibility changes BEFORE they happen (pre-show/hide "swizzle") + WINDOWPOS* pos = reinterpret_cast(lParam); + if (pos) { + // Get window ID from window's custom property (stored during window creation) + HANDLE prop_handle = GetPropW(hwnd, kWindowIdProperty); + if (prop_handle) { + WindowId window_id = static_cast(reinterpret_cast(prop_handle)); + if (window_id != IdAllocator::kInvalidId) { + auto& manager = WindowManager::GetInstance(); + bool hook_handled = false; + + if (pos->flags & SWP_SHOWWINDOW) { + if (manager.HasWillShowHook()) { + manager.HandleWillShow(window_id); + hook_handled = true; + } + } + if (pos->flags & SWP_HIDEWINDOW) { + if (manager.HasWillHideHook()) { + manager.HandleWillHide(window_id); + hook_handled = true; + } + } + + // If hook handled it, cancel the visibility change + if (hook_handled) { + pos->flags &= ~(SWP_SHOWWINDOW | SWP_HIDEWINDOW); + } + } + } + } + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + case WM_SHOWWINDOW: + return DefWindowProc(hwnd, uMsg, wParam, lParam); + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } +} + +Window::Window() { + // Create a new window with default settings + HINSTANCE hInstance = GetModuleHandle(nullptr); + + // Register window class if not already registered + static bool class_registered = false; + static std::wstring wclass_name = StringToWString("NativeAPIWindow"); + + if (!class_registered) { + WNDCLASSW wc = {}; + wc.lpfnWndProc = WindowProc; + wc.hInstance = hInstance; + wc.lpszClassName = wclass_name.c_str(); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + + if (RegisterClassW(&wc)) { + class_registered = true; + } else { + DWORD error = GetLastError(); + if (error != ERROR_CLASS_ALREADY_EXISTS) { + std::cerr << "Failed to register window class. Error: " << error << std::endl; + // Allocate ID even for failed window creation to maintain consistency + WindowId id = IdAllocator::Allocate(); + pimpl_ = std::make_unique(nullptr, id); + return; + } + class_registered = true; + } + } + + // Create the window + DWORD style = WS_OVERLAPPEDWINDOW; + DWORD exStyle = 0; + + HWND hwnd = CreateWindowExW(exStyle, wclass_name.c_str(), L"", style, CW_USEDEFAULT, + CW_USEDEFAULT, 800, 600, nullptr, nullptr, hInstance, nullptr); + + if (!hwnd) { + std::cerr << "Failed to create window. Error: " << GetLastError() << std::endl; + // Allocate ID even for failed window creation to maintain consistency + WindowId id = IdAllocator::Allocate(); + pimpl_ = std::make_unique(nullptr, id); + return; + } + + // Allocate window ID using IdAllocator + WindowId id = IdAllocator::Allocate(); + if (id == IdAllocator::kInvalidId) { + std::cerr << "Failed to allocate window ID" << std::endl; + DestroyWindow(hwnd); + pimpl_ = std::make_unique(nullptr, IdAllocator::kInvalidId); + return; + } + + // Store window ID as a custom property in HWND for easy retrieval in WindowProc + SetPropW(hwnd, kWindowIdProperty, reinterpret_cast(static_cast(id))); + + // Create the instance with allocated ID + pimpl_ = std::make_unique(hwnd, id); + + // Note: Window registration in WindowRegistry is now handled by WindowManager::GetAll() + // which uses EnumWindows to discover and register all windows dynamically +} + +Window::Window(void* native_window) { + HWND hwnd = static_cast(native_window); + + if (!hwnd) { + // Allocate ID even for null window to maintain consistency + WindowId id = IdAllocator::Allocate(); + pimpl_ = std::make_unique(nullptr, id); + return; + } + + // Check if window already has an ID stored as a custom property + HANDLE prop_handle = GetPropW(hwnd, kWindowIdProperty); + WindowId id = IdAllocator::kInvalidId; + + if (prop_handle) { + id = static_cast(reinterpret_cast(prop_handle)); + } + + if (id == IdAllocator::kInvalidId || id == 0) { + // Allocate new ID if window doesn't have one + id = IdAllocator::Allocate(); + if (id == IdAllocator::kInvalidId) { + std::cerr << "Failed to allocate window ID" << std::endl; + pimpl_ = std::make_unique(nullptr, IdAllocator::kInvalidId); + return; + } + // Store the ID as a custom property in HWND + SetPropW(hwnd, kWindowIdProperty, reinterpret_cast(static_cast(id))); + } + + pimpl_ = std::make_unique(hwnd, id); + + // Note: Window registration in WindowRegistry is now handled by WindowManager::GetAll() + // which uses EnumWindows to discover and register all windows dynamically +} + +Window::~Window() { + if (pimpl_ && pimpl_->window_id_ != IdAllocator::kInvalidId) { + // Unregister WM_GETMINMAXINFO handler if registered + if (pimpl_->min_max_handler_id_ != 0 && pimpl_->hwnd_) { + WindowMessageDispatcher::GetInstance().UnregisterHandler( + pimpl_->min_max_handler_id_); + } + + // Remove window from registry on destruction + WindowRegistry::GetInstance().Remove(pimpl_->window_id_); + + // Remove the custom property from HWND if window is still valid + if (pimpl_->hwnd_) { + RemovePropW(pimpl_->hwnd_, kWindowIdProperty); + } + } +} + +void Window::Focus() { + if (pimpl_->hwnd_) { + SetForegroundWindow(pimpl_->hwnd_); + SetFocus(pimpl_->hwnd_); + } +} + +void Window::Blur() { + if (pimpl_->hwnd_) { + SetFocus(nullptr); + } +} + +bool Window::IsFocused() const { + return pimpl_->hwnd_ && GetForegroundWindow() == pimpl_->hwnd_; +} + +void Window::Show() { + if (pimpl_->hwnd_) { + ShowWindow(pimpl_->hwnd_, SW_SHOW); + SetForegroundWindow(pimpl_->hwnd_); + } +} + +void Window::ShowInactive() { + if (pimpl_->hwnd_) { + ShowWindow(pimpl_->hwnd_, SW_SHOWNOACTIVATE); + } +} + +void Window::Hide() { + if (pimpl_->hwnd_) { + ShowWindow(pimpl_->hwnd_, SW_HIDE); + } +} + +bool Window::IsVisible() const { + return pimpl_->hwnd_ && IsWindowVisible(pimpl_->hwnd_); +} + +void Window::Maximize() { + if (pimpl_->hwnd_ && !IsMaximized()) { + ShowWindow(pimpl_->hwnd_, SW_MAXIMIZE); + } +} + +void Window::Unmaximize() { + if (pimpl_->hwnd_ && IsMaximized()) { + ShowWindow(pimpl_->hwnd_, SW_RESTORE); + } +} + +bool Window::IsMaximized() const { + if (!pimpl_->hwnd_) + return false; + WINDOWPLACEMENT wp = {}; + wp.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(pimpl_->hwnd_, &wp); + return wp.showCmd == SW_MAXIMIZE; +} + +void Window::Minimize() { + if (pimpl_->hwnd_ && !IsMinimized()) { + ShowWindow(pimpl_->hwnd_, SW_MINIMIZE); + } +} + +void Window::Restore() { + if (pimpl_->hwnd_ && IsMinimized()) { + ShowWindow(pimpl_->hwnd_, SW_RESTORE); + } +} + +bool Window::IsMinimized() const { + if (!pimpl_->hwnd_) + return false; + WINDOWPLACEMENT wp = {}; + wp.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(pimpl_->hwnd_, &wp); + return wp.showCmd == SW_MINIMIZE; +} + +void Window::SetFullScreen(bool is_full_screen) { + if (!pimpl_->hwnd_) + return; + + static WINDOWPLACEMENT g_wpPrev = {sizeof(g_wpPrev)}; + static DWORD g_dwStyle = 0; + static DWORD g_dwExStyle = 0; + + if (is_full_screen) { + if (!IsFullScreen()) { + // Save current window placement and style + GetWindowPlacement(pimpl_->hwnd_, &g_wpPrev); + g_dwStyle = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + g_dwExStyle = GetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE); + + // Remove window decorations + SetWindowLong(pimpl_->hwnd_, GWL_STYLE, g_dwStyle & ~(WS_CAPTION | WS_THICKFRAME)); + SetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE, + g_dwExStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | + WS_EX_STATICEDGE)); + + // Get monitor info + MONITORINFO mi = {sizeof(mi)}; + GetMonitorInfo(MonitorFromWindow(pimpl_->hwnd_, MONITOR_DEFAULTTONEAREST), &mi); + + // Set window to cover entire monitor + SetWindowPos(pimpl_->hwnd_, nullptr, mi.rcMonitor.left, mi.rcMonitor.top, + mi.rcMonitor.right - mi.rcMonitor.left, mi.rcMonitor.bottom - mi.rcMonitor.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } + } else { + if (IsFullScreen()) { + // Restore window style and placement + SetWindowLong(pimpl_->hwnd_, GWL_STYLE, g_dwStyle); + SetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE, g_dwExStyle); + SetWindowPlacement(pimpl_->hwnd_, &g_wpPrev); + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + } + } +} + +bool Window::IsFullScreen() const { + if (!pimpl_->hwnd_) + return false; + + RECT windowRect, monitorRect; + GetWindowRect(pimpl_->hwnd_, &windowRect); + + MONITORINFO mi = {sizeof(mi)}; + GetMonitorInfo(MonitorFromWindow(pimpl_->hwnd_, MONITOR_DEFAULTTONEAREST), &mi); + monitorRect = mi.rcMonitor; + + return (windowRect.left == monitorRect.left && windowRect.top == monitorRect.top && + windowRect.right == monitorRect.right && windowRect.bottom == monitorRect.bottom); +} + +void Window::SetBounds(Rectangle bounds) { + if (pimpl_->hwnd_) { + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + SetWindowPos(pimpl_->hwnd_, nullptr, + static_cast(std::lround(bounds.x * scale)), + static_cast(std::lround(bounds.y * scale)), + static_cast(std::lround(bounds.width * scale)), + static_cast(std::lround(bounds.height * scale)), SWP_NOZORDER); + } +} + +Rectangle Window::GetBounds() const { + Rectangle bounds = {0, 0, 0, 0}; + if (pimpl_->hwnd_) { + RECT rect; + GetWindowRect(pimpl_->hwnd_, &rect); + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + bounds.x = static_cast(rect.left) / scale; + bounds.y = static_cast(rect.top) / scale; + bounds.width = static_cast(rect.right - rect.left) / scale; + bounds.height = static_cast(rect.bottom - rect.top) / scale; + } + return bounds; +} + +void Window::SetSize(Size size, bool animate) { + if (pimpl_->hwnd_) { + // Windows doesn't have built-in animation for window resizing + // Animation would require custom implementation + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, + static_cast(std::lround(size.width * scale)), + static_cast(std::lround(size.height * scale)), + SWP_NOMOVE | SWP_NOZORDER); + } +} + +Size Window::GetSize() const { + Size size = {0, 0}; + if (pimpl_->hwnd_) { + RECT rect; + GetWindowRect(pimpl_->hwnd_, &rect); + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + size.width = static_cast(rect.right - rect.left) / scale; + size.height = static_cast(rect.bottom - rect.top) / scale; + } + return size; +} + +void Window::SetContentSize(Size size) { + if (pimpl_->hwnd_) { + RECT windowRect, clientRect; + GetWindowRect(pimpl_->hwnd_, &windowRect); + GetClientRect(pimpl_->hwnd_, &clientRect); + + // Calculate the difference between window and client area + int borderWidth = (windowRect.right - windowRect.left) - clientRect.right; + int borderHeight = (windowRect.bottom - windowRect.top) - clientRect.bottom; + + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, + static_cast(std::lround(size.width * scale)) + borderWidth, + static_cast(std::lround(size.height * scale)) + borderHeight, + SWP_NOMOVE | SWP_NOZORDER); + } +} + +Size Window::GetContentSize() const { + Size size = {0, 0}; + if (pimpl_->hwnd_) { + RECT rect; + GetClientRect(pimpl_->hwnd_, &rect); + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + size.width = static_cast(rect.right) / scale; + size.height = static_cast(rect.bottom) / scale; + } + return size; +} + +void Window::SetContentBounds(Rectangle bounds) { + if (pimpl_->hwnd_) { + RECT windowRect, clientRect; + GetWindowRect(pimpl_->hwnd_, &windowRect); + GetClientRect(pimpl_->hwnd_, &clientRect); + + // Calculate the difference between window and client area + int borderWidth = (windowRect.right - windowRect.left) - clientRect.right; + int borderHeight = (windowRect.bottom - windowRect.top) - clientRect.bottom; + + // Get current client area position in screen coordinates + POINT clientTopLeft = {0, 0}; + ClientToScreen(pimpl_->hwnd_, &clientTopLeft); + + // Calculate the offset from window top-left to client top-left + int offsetX = clientTopLeft.x - windowRect.left; + int offsetY = clientTopLeft.y - windowRect.top; + + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + + // Calculate window position so that client area is at bounds position + int windowX = static_cast(std::lround(bounds.x * scale)) - offsetX; + int windowY = static_cast(std::lround(bounds.y * scale)) - offsetY; + int windowWidth = static_cast(std::lround(bounds.width * scale)) + borderWidth; + int windowHeight = static_cast(std::lround(bounds.height * scale)) + borderHeight; + + SetWindowPos(pimpl_->hwnd_, nullptr, windowX, windowY, windowWidth, windowHeight, SWP_NOZORDER); + } +} + +Rectangle Window::GetContentBounds() const { + Rectangle bounds = {0, 0, 0, 0}; + if (pimpl_->hwnd_) { + RECT clientRect; + GetClientRect(pimpl_->hwnd_, &clientRect); + + // Convert client rect to screen coordinates (physical pixels) + POINT topLeft = {clientRect.left, clientRect.top}; + POINT bottomRight = {clientRect.right, clientRect.bottom}; + ClientToScreen(pimpl_->hwnd_, &topLeft); + ClientToScreen(pimpl_->hwnd_, &bottomRight); + + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + + // Return logical pixels (DIP) by dividing by scale + bounds.x = static_cast(topLeft.x) / scale; + bounds.y = static_cast(topLeft.y) / scale; + bounds.width = static_cast(bottomRight.x - topLeft.x) / scale; + bounds.height = static_cast(bottomRight.y - topLeft.y) / scale; + } + return bounds; +} + +// Helper function: registers a WM_GETMINMAXINFO handler for the given HWND +// via WindowMessageDispatcher if not already registered. Returns the handler ID. +static int RegisterMinMaxInfoHandler(HWND hwnd, int existing_handler_id) { + if (existing_handler_id != 0) { + return existing_handler_id; + } + if (!hwnd || !IsWindow(hwnd)) { + return 0; + } + auto& dispatcher = WindowMessageDispatcher::GetInstance(); + return dispatcher.RegisterHandler( + hwnd, + [](HWND hwnd, UINT msg, WPARAM wparam, + LPARAM lparam) -> std::optional { + if (msg == WM_GETMINMAXINFO) { + HANDLE prop_handle = GetPropW(hwnd, kWindowIdProperty); + if (prop_handle) { + WindowId window_id = static_cast( + reinterpret_cast(prop_handle)); + if (window_id != IdAllocator::kInvalidId) { + auto window = WindowRegistry::GetInstance().Get(window_id); + if (window) { + auto minSize = window->GetMinimumSize(); + auto maxSize = window->GetMaximumSize(); + MINMAXINFO* mmi = reinterpret_cast(lparam); + double scale_mm = GetScaleFactorForWindow(hwnd); + if (scale_mm <= 0.0) + scale_mm = 1.0; + if (minSize.width > 0 && minSize.height > 0) { + mmi->ptMinTrackSize.x = static_cast(std::lround(minSize.width * scale_mm)); + mmi->ptMinTrackSize.y = static_cast(std::lround(minSize.height * scale_mm)); + } + if (maxSize.width > 0 && maxSize.height > 0) { + mmi->ptMaxTrackSize.x = static_cast(std::lround(maxSize.width * scale_mm)); + mmi->ptMaxTrackSize.y = static_cast(std::lround(maxSize.height * scale_mm)); + } + return std::make_optional(0); + } + } + } + } + return std::nullopt; + }); +} + +void Window::SetMinimumSize(Size size) { + pimpl_->min_size_ = size; + + if (pimpl_->hwnd_) { + pimpl_->min_max_handler_id_ = + RegisterMinMaxInfoHandler(pimpl_->hwnd_, pimpl_->min_max_handler_id_); + + // Trigger the window to re-evaluate its size constraints + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER); + } +} + +Size Window::GetMinimumSize() const { + return pimpl_->min_size_; +} + +void Window::SetMaximumSize(Size size) { + pimpl_->max_size_ = size; + + if (pimpl_->hwnd_) { + pimpl_->min_max_handler_id_ = + RegisterMinMaxInfoHandler(pimpl_->hwnd_, pimpl_->min_max_handler_id_); + + // Trigger the window to re-evaluate its size constraints + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER); + } +} + +Size Window::GetMaximumSize() const { + return pimpl_->max_size_; +} + +void Window::SetResizable(bool is_resizable) { + if (pimpl_->hwnd_) { + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + if (is_resizable) { + style |= WS_THICKFRAME | WS_MAXIMIZEBOX; + } else { + style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); + } + SetWindowLong(pimpl_->hwnd_, GWL_STYLE, style); + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } +} + +bool Window::IsResizable() const { + if (!pimpl_->hwnd_) + return false; + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + return (style & WS_THICKFRAME) != 0; +} + +void Window::SetMovable(bool is_movable) { + // Windows doesn't have a direct way to disable window movement + // This would require custom window procedure handling +} + +bool Window::IsMovable() const { + // Windows windows are movable by default + return true; +} + +void Window::SetMinimizable(bool is_minimizable) { + if (pimpl_->hwnd_) { + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + if (is_minimizable) { + style |= WS_MINIMIZEBOX; + } else { + style &= ~WS_MINIMIZEBOX; + } + SetWindowLong(pimpl_->hwnd_, GWL_STYLE, style); + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } +} + +bool Window::IsMinimizable() const { + if (!pimpl_->hwnd_) + return false; + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + return (style & WS_MINIMIZEBOX) != 0; +} + +void Window::SetMaximizable(bool is_maximizable) { + if (pimpl_->hwnd_) { + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + if (is_maximizable) { + style |= WS_MAXIMIZEBOX; + } else { + style &= ~WS_MAXIMIZEBOX; + } + SetWindowLong(pimpl_->hwnd_, GWL_STYLE, style); + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } +} + +bool Window::IsMaximizable() const { + if (!pimpl_->hwnd_) + return false; + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + return (style & WS_MAXIMIZEBOX) != 0; +} + +void Window::SetFullScreenable(bool is_full_screenable) { + // This is a concept more relevant to macOS + // On Windows, any window can potentially go fullscreen +} + +bool Window::IsFullScreenable() const { + return true; // All Windows windows can go fullscreen +} + +void Window::SetClosable(bool is_closable) { + if (pimpl_->hwnd_) { + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + if (is_closable) { + style |= WS_SYSMENU; + } else { + style &= ~WS_SYSMENU; + } + SetWindowLong(pimpl_->hwnd_, GWL_STYLE, style); + SetWindowPos(pimpl_->hwnd_, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + } +} + +bool Window::IsClosable() const { + if (!pimpl_->hwnd_) + return false; + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + return (style & WS_SYSMENU) != 0; +} + +void Window::SetWindowControlButtonsVisible(bool is_visible) { + // TODO: Implement for Windows + // This would involve custom window chrome or DWM frame manipulation +} + +bool Window::IsWindowControlButtonsVisible() const { + // TODO: Implement for Windows + return true; // Default to visible +} + +void Window::SetAlwaysOnTop(bool is_always_on_top) { + if (pimpl_->hwnd_) { + SetWindowPos(pimpl_->hwnd_, is_always_on_top ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE); + } +} + +bool Window::IsAlwaysOnTop() const { + if (!pimpl_->hwnd_) + return false; + LONG exStyle = GetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE); + return (exStyle & WS_EX_TOPMOST) != 0; +} + +void Window::SetPosition(Point point) { + if (pimpl_->hwnd_) { + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + SetWindowPos(pimpl_->hwnd_, nullptr, + static_cast(std::lround(point.x * scale)), + static_cast(std::lround(point.y * scale)), + 0, 0, SWP_NOSIZE | SWP_NOZORDER); + } +} + +Point Window::GetPosition() const { + Point point = {0, 0}; + if (pimpl_->hwnd_) { + RECT rect; + GetWindowRect(pimpl_->hwnd_, &rect); + double scale = GetScaleFactorForWindow(pimpl_->hwnd_); + if (scale <= 0.0) + scale = 1.0; + point.x = static_cast(rect.left) / scale; + point.y = static_cast(rect.top) / scale; + } + return point; +} + +void Window::Center() { + if (!pimpl_->hwnd_) + return; + + // Get the current window size + RECT windowRect; + GetWindowRect(pimpl_->hwnd_, &windowRect); + int windowWidth = windowRect.right - windowRect.left; + int windowHeight = windowRect.bottom - windowRect.top; + + // Get the monitor that the window is currently on + HMONITOR monitor = MonitorFromWindow(pimpl_->hwnd_, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = {sizeof(mi)}; + GetMonitorInfo(monitor, &mi); + + // Calculate the center position on the monitor's work area + // All values here are in physical pixels (GetWindowRect and rcWork), so no DPI scaling needed + int centerX = mi.rcWork.left + (mi.rcWork.right - mi.rcWork.left - windowWidth) / 2; + int centerY = mi.rcWork.top + (mi.rcWork.bottom - mi.rcWork.top - windowHeight) / 2; + + // Set the window position to center + SetWindowPos(pimpl_->hwnd_, nullptr, centerX, centerY, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); +} + +void Window::SetTitle(std::string title) { + if (pimpl_->hwnd_) { + std::wstring wtitle = StringToWString(title); + SetWindowTextW(pimpl_->hwnd_, wtitle.c_str()); + } +} + +std::string Window::GetTitle() const { + if (!pimpl_->hwnd_) + return ""; + + int length = GetWindowTextLengthW(pimpl_->hwnd_); + if (length == 0) + return ""; + + std::wstring wtitle(length + 1, L'\0'); + GetWindowTextW(pimpl_->hwnd_, &wtitle[0], length + 1); + wtitle.resize(length); + return WStringToString(wtitle); +} + +void Window::SetTitleBarStyle(TitleBarStyle style) { + if (!pimpl_->hwnd_) + return; + + pimpl_->title_bar_style_ = style; + + // Get current window rect + RECT rect; + GetWindowRect(pimpl_->hwnd_, &rect); + + // Apply DWM frame extension based on style + MARGINS margins = {0, 0, 0, 0}; + DwmExtendFrameIntoClientArea(pimpl_->hwnd_, &margins); + + // Trigger frame change to apply the new style + SetWindowPos(pimpl_->hwnd_, nullptr, rect.left, rect.top, 0, 0, + SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); +} + +TitleBarStyle Window::GetTitleBarStyle() const { + return pimpl_->title_bar_style_; +} + +void Window::SetHasShadow(bool has_shadow) { + // Windows shadow is typically handled automatically + // Custom shadow implementation would be complex +} + +bool Window::HasShadow() const { + return true; // Windows typically have shadows by default +} + +void Window::SetOpacity(float opacity) { + if (pimpl_->hwnd_) { + LONG exStyle = GetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE); + + if (opacity < 1.0f) { + // Enable layered window and set opacity + SetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE, exStyle | WS_EX_LAYERED); + SetLayeredWindowAttributes(pimpl_->hwnd_, 0, static_cast(opacity * 255), LWA_ALPHA); + } else { + // Disable layered window + SetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED); + } + } +} + +float Window::GetOpacity() const { + if (!pimpl_->hwnd_) + return 1.0f; + + LONG exStyle = GetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE); + if (exStyle & WS_EX_LAYERED) { + BYTE alpha; + if (GetLayeredWindowAttributes(pimpl_->hwnd_, nullptr, &alpha, nullptr)) { + return alpha / 255.0f; + } + } + return 1.0f; +} + +void Window::SetVisualEffect(VisualEffect effect) { + if (!pimpl_->hwnd_ || pimpl_->visual_effect_ == effect) + return; + + pimpl_->visual_effect_ = effect; + + // DWM_SYSTEMBACKDROP_TYPE is available in Windows 11 Build 22621+ + // DWMWA_SYSTEMBACKDROP_TYPE = 38 + int backdrop_type = 1; // DWMSBT_NONE + + switch (effect) { + case VisualEffect::None: + backdrop_type = 1; // DWMSBT_NONE + break; + case VisualEffect::Blur: + case VisualEffect::Acrylic: + backdrop_type = 3; // DWMSBT_TRANSIENTWINDOW (Acrylic) + break; + case VisualEffect::Mica: + backdrop_type = 2; // DWMSBT_MAINWINDOW (Mica) + break; + } + + DwmSetWindowAttribute(pimpl_->hwnd_, 38, &backdrop_type, sizeof(backdrop_type)); +} + +VisualEffect Window::GetVisualEffect() const { + return pimpl_->visual_effect_; +} + +void Window::SetBackgroundColor(const Color& color) { + if (!pimpl_->hwnd_) + return; + + // Create new brush with the specified color + COLORREF colorRef = RGB(color.r, color.g, color.b); + HBRUSH brush = CreateSolidBrush(colorRef); + + // Get old brush to delete it later + HBRUSH oldBrush = reinterpret_cast( + SetClassLongPtr(pimpl_->hwnd_, GCLP_HBRBACKGROUND, + reinterpret_cast(brush))); + + // Delete old brush if it's not a system brush + if (oldBrush && oldBrush != GetStockObject(NULL_BRUSH) && + oldBrush != GetStockObject(WHITE_BRUSH) && + oldBrush != GetStockObject(BLACK_BRUSH) && + oldBrush != GetStockObject(GRAY_BRUSH) && + oldBrush != GetStockObject(LTGRAY_BRUSH) && + oldBrush != GetStockObject(DKGRAY_BRUSH)) { + DeleteObject(oldBrush); + } + + // Force window to redraw with new background color + InvalidateRect(pimpl_->hwnd_, nullptr, TRUE); +} + +Color Window::GetBackgroundColor() const { + if (!pimpl_->hwnd_) + return Color::White; + + // Get the background brush from the window class + HBRUSH brush = reinterpret_cast( + GetClassLongPtr(pimpl_->hwnd_, GCLP_HBRBACKGROUND)); + + if (!brush || brush == GetStockObject(NULL_BRUSH)) { + return Color::White; + } + + // Get the brush color using GetObject + LOGBRUSH logBrush; + if (GetObject(brush, sizeof(LOGBRUSH), &logBrush) == 0) { + return Color::White; + } + + // Extract RGB values from COLORREF + COLORREF colorRef = logBrush.lbColor; + return Color::FromRGBA( + GetRValue(colorRef), + GetGValue(colorRef), + GetBValue(colorRef), + 255 // Windows doesn't store alpha in solid brush + ); +} + +void Window::SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces) { + // Windows doesn't have the same concept of workspaces as macOS + // This would require integration with virtual desktop APIs +} + +bool Window::IsVisibleOnAllWorkspaces() const { + return false; // Not supported on Windows by default +} + +void Window::SetIgnoreMouseEvents(bool is_ignore_mouse_events) { + if (pimpl_->hwnd_) { + LONG exStyle = GetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE); + if (is_ignore_mouse_events) { + exStyle |= WS_EX_TRANSPARENT; + } else { + exStyle &= ~WS_EX_TRANSPARENT; + } + SetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE, exStyle); + } +} + +bool Window::IsIgnoreMouseEvents() const { + if (!pimpl_->hwnd_) + return false; + LONG exStyle = GetWindowLong(pimpl_->hwnd_, GWL_EXSTYLE); + return (exStyle & WS_EX_TRANSPARENT) != 0; +} + +void Window::SetFocusable(bool is_focusable) { + // Windows focusability is typically controlled by window style + // This is a simplified implementation +} + +bool Window::IsFocusable() const { + if (!pimpl_->hwnd_) + return false; + LONG style = GetWindowLong(pimpl_->hwnd_, GWL_STYLE); + return (style & WS_DISABLED) == 0; +} + +void Window::StartDragging() { + if (pimpl_->hwnd_) { + // Simulate dragging by sending WM_NCLBUTTONDOWN with HTCAPTION + PostMessage(pimpl_->hwnd_, WM_NCLBUTTONDOWN, HTCAPTION, 0); + } +} + +void Window::StartResizing() { + // Windows doesn't have a direct API to start resizing programmatically + // This would require more complex implementation +} + +WindowId Window::GetId() const { + if (!pimpl_) { + return IdAllocator::kInvalidId; + } + return pimpl_->window_id_; +} + +void* Window::GetNativeObjectInternal() const { + return pimpl_ ? reinterpret_cast(pimpl_->hwnd_) : nullptr; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/positioning_strategy.cpp b/packages/cnativeapi/cxx_impl/src/positioning_strategy.cpp new file mode 100644 index 0000000..ef98ae1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/positioning_strategy.cpp @@ -0,0 +1,46 @@ +#include "positioning_strategy.h" +#include "window.h" + +namespace nativeapi { + +PositioningStrategy::PositioningStrategy(Type type) + : type_(type), + absolute_position_{0, 0}, + relative_rect_{0, 0, 0, 0}, + relative_offset_{0, 0}, + relative_window_(nullptr) {} + +PositioningStrategy PositioningStrategy::Absolute(const Point& point) { + PositioningStrategy strategy(Type::Absolute); + strategy.absolute_position_ = point; + return strategy; +} + +PositioningStrategy PositioningStrategy::CursorPosition() { + return PositioningStrategy(Type::CursorPosition); +} + +PositioningStrategy PositioningStrategy::Relative(const Rectangle& rect, const Point& offset) { + PositioningStrategy strategy(Type::Relative); + strategy.relative_rect_ = rect; + strategy.relative_offset_ = offset; + strategy.relative_window_ = nullptr; + return strategy; +} + +PositioningStrategy PositioningStrategy::Relative(const Window& window, const Point& offset) { + PositioningStrategy strategy(Type::Relative); + strategy.relative_window_ = &window; + strategy.relative_offset_ = offset; + // relative_rect_ will be obtained dynamically in GetRelativeRectangle() + return strategy; +} + +Rectangle PositioningStrategy::GetRelativeRectangle() const { + if (relative_window_) { + return relative_window_->GetContentBounds(); + } + return relative_rect_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/positioning_strategy.h b/packages/cnativeapi/cxx_impl/src/positioning_strategy.h new file mode 100644 index 0000000..0980185 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/positioning_strategy.h @@ -0,0 +1,180 @@ +#pragma once +#include "foundation/geometry.h" + +namespace nativeapi { + +// Forward declaration +class Window; + +/** + * @brief Strategy for determining where to position UI elements. + * + * PositioningStrategy defines how to calculate the position for UI elements + * such as menus, tooltips, or popovers. It supports various positioning modes: + * - Absolute: Fixed screen coordinates + * - CursorPosition: Current mouse cursor position + * - Relative: Position relative to a rectangle + * + * @example + * ```cpp + * // Position menu at absolute screen coordinates + * menu->Open(PositioningStrategy::Absolute({100, 200})); + * + * // Position menu at current mouse location + * menu->Open(PositioningStrategy::CursorPosition()); + * + * // Position menu relative to a rectangle with offset + * Rectangle buttonRect = button->GetBounds(); + * menu->Open(PositioningStrategy::Relative(buttonRect, {0, 10})); + * ``` + */ +class PositioningStrategy { + public: + /** + * @brief Type of positioning strategy. + */ + enum class Type { + /** + * Position at fixed screen coordinates. + */ + Absolute, + + /** + * Position at current mouse cursor location. + */ + CursorPosition, + + /** + * Position relative to a rectangle. + */ + Relative + }; + + /** + * @brief Create a strategy for absolute positioning at fixed coordinates. + * + * @param point Point in screen coordinates + * @return PositioningStrategy configured for absolute positioning + * + * @example + * ```cpp + * auto strategy = PositioningStrategy::Absolute({100, 200}); + * menu->Open(strategy, Placement::Bottom); + * ``` + */ + static PositioningStrategy Absolute(const Point& point); + + /** + * @brief Create a strategy for positioning at current mouse location. + * + * @return PositioningStrategy configured to use mouse cursor position + * + * @example + * ```cpp + * auto strategy = PositioningStrategy::CursorPosition(); + * contextMenu->Open(strategy, Placement::BottomStart); + * ``` + */ + static PositioningStrategy CursorPosition(); + + /** + * @brief Create a strategy for positioning relative to a rectangle. + * + * @param rect Rectangle in screen coordinates to position relative to + * @param offset Optional offset point to apply to the position (default: {0, 0}) + * @return PositioningStrategy configured for rectangle-relative positioning + * + * @example + * ```cpp + * Rectangle buttonRect = button->GetBounds(); + * // Position at bottom of button (no offset) + * auto strategy = PositioningStrategy::Relative(buttonRect, {0, 0}); + * menu->Open(strategy); + * + * // Position at bottom of button with 10px vertical offset + * auto strategy2 = PositioningStrategy::Relative(buttonRect, {0, 10}); + * menu->Open(strategy2); + * ``` + */ + static PositioningStrategy Relative(const Rectangle& rect, const Point& offset = {0, 0}); + + /** + * @brief Create a strategy for positioning relative to a window. + * + * @param window Window to position relative to + * @param offset Optional offset point to apply to the position (default: {0, 0}) + * @return PositioningStrategy configured for window-relative positioning + * + * This method stores a reference to the window and will obtain its bounds + * dynamically when GetRelativeRectangle() is called, ensuring the position + * reflects the window's current state. + * + * @example + * ```cpp + * auto window = WindowManager::GetInstance().Create(options); + * // Position menu at bottom of window (no offset) + * auto strategy = PositioningStrategy::Relative(*window, {0, 0}); + * menu->Open(strategy); + * + * // Position menu at bottom of window with 10px vertical offset + * auto strategy2 = PositioningStrategy::Relative(*window, {0, 10}); + * menu->Open(strategy2); + * ``` + */ + static PositioningStrategy Relative(const Window& window, const Point& offset = {0, 0}); + + /** + * @brief Get the type of this positioning strategy. + * + * @return The Type enum value indicating the strategy type + */ + Type GetType() const { return type_; } + + /** + * @brief Get the absolute position (for Absolute type). + * + * @return Point containing x,y coordinates + * @note Only valid when GetType() == Type::Absolute + */ + Point GetAbsolutePosition() const { return absolute_position_; } + + /** + * @brief Get the relative rectangle (for Relative type). + * + * @return Rectangle in screen coordinates + * @note Only valid when GetType() == Type::Relative + * @note If the strategy was created with a Window, this will return the + * window's current bounds (obtained dynamically). + */ + Rectangle GetRelativeRectangle() const; + + /** + * @brief Get the relative offset point (for Relative type). + * + * @return Point containing x,y relative offset + * @note Only valid when GetType() == Type::Relative + */ + Point GetRelativeOffset() const { return relative_offset_; } + + /** + * @brief Get the relative window (for Relative type created with Window). + * + * @return Pointer to the Window, or nullptr if not set + * @note Only valid when GetType() == Type::Relative and strategy was created with a Window + */ + const Window* GetRelativeWindow() const { return relative_window_; } + + private: + /** + * @brief Private constructor - use static factory methods instead. + */ + PositioningStrategy(Type type); + + Type type_; + Point absolute_position_; + Rectangle relative_rect_; + Point relative_offset_; + const Window* relative_window_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/preferences.cpp b/packages/cnativeapi/cxx_impl/src/preferences.cpp new file mode 100644 index 0000000..f462c64 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/preferences.cpp @@ -0,0 +1,14 @@ +#include "preferences.h" + +// Platform-specific implementation is in platform directories +// Note: All methods including constructors, destructors, and GetNamespace() +// are defined in platform-specific files + +namespace nativeapi { + +// All implementations are in platform-specific files: +// - platform/windows/preferences_windows.cpp +// - platform/macos/preferences_macos.mm +// - platform/linux/preferences_linux.cpp + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/preferences.h b/packages/cnativeapi/cxx_impl/src/preferences.h new file mode 100644 index 0000000..dbf1431 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/preferences.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include "storage.h" + +namespace nativeapi { + +/** + * @brief General-purpose key-value storage for application preferences. + * + * Similar to Web Storage's localStorage, this provides persistent storage + * for non-sensitive application data. Data is stored in plain text. + * + * Platform implementations: + * - Windows: Registry (HKEY_CURRENT_USER) or INI files + * - macOS: NSUserDefaults + * - Linux: Configuration files (XDG Base Directory) + * + * @warning Do not store sensitive data (passwords, tokens) here. + * Use SecureStorage for sensitive data. + */ +class Preferences : public Storage { + public: + /** + * @brief Create preferences storage with default scope. + * + * Uses application name as scope if available. + */ + Preferences(); + + /** + * @brief Create preferences storage with custom scope. + * + * @param scope Scope for isolating preferences + */ + explicit Preferences(const std::string& scope); + + virtual ~Preferences(); + + // Storage interface implementation + bool Set(const std::string& key, const std::string& value) override; + std::string Get(const std::string& key, const std::string& default_value = "") const override; + bool Remove(const std::string& key) override; + bool Clear() override; + bool Contains(const std::string& key) const override; + std::vector GetKeys() const override; + size_t GetSize() const override; + std::map GetAll() const override; + + /** + * @brief Get the scope. + * + * @return The scope used for this preferences instance + */ + std::string GetScope() const; + + // Prevent copying and moving + Preferences(const Preferences&) = delete; + Preferences& operator=(const Preferences&) = delete; + Preferences(Preferences&&) = delete; + Preferences& operator=(Preferences&&) = delete; + + private: + class Impl; + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/secure_storage.cpp b/packages/cnativeapi/cxx_impl/src/secure_storage.cpp new file mode 100644 index 0000000..66253a1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/secure_storage.cpp @@ -0,0 +1,14 @@ +#include "secure_storage.h" + +// Platform-specific implementation is in platform directories +// Note: All methods including constructors, destructors, GetServiceName(), +// and IsAvailable() are defined in platform-specific files + +namespace nativeapi { + +// All implementations are in platform-specific files: +// - platform/windows/secure_storage_windows.cpp +// - platform/macos/secure_storage_macos.mm +// - platform/linux/secure_storage_linux.cpp + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/secure_storage.h b/packages/cnativeapi/cxx_impl/src/secure_storage.h new file mode 100644 index 0000000..a08d523 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/secure_storage.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include "storage.h" + +namespace nativeapi { + +/** + * @brief Secure storage for sensitive data like passwords and tokens. + * + * Similar to Web Storage's encrypted storage, this provides secure persistent + * storage for sensitive application data. Data is encrypted at rest. + * + * Platform implementations: + * - Windows: Credential Manager (Windows Data Protection API) + * - macOS: Keychain Services + * - Linux: libsecret (GNOME Keyring) or encrypted files + * + * @warning This is a stub implementation. Actual encryption is not yet implemented. + */ +class SecureStorage : public Storage { + public: + /** + * @brief Create secure storage with default scope. + * + * Uses application name as scope identifier if available. + */ + SecureStorage(); + + /** + * @brief Create secure storage with custom scope. + * + * @param scope Scope/application identifier for keychain/credential manager + */ + explicit SecureStorage(const std::string& scope); + + virtual ~SecureStorage(); + + // Storage interface implementation + bool Set(const std::string& key, const std::string& value) override; + std::string Get(const std::string& key, const std::string& default_value = "") const override; + bool Remove(const std::string& key) override; + bool Clear() override; + bool Contains(const std::string& key) const override; + std::vector GetKeys() const override; + size_t GetSize() const override; + std::map GetAll() const override; + + /** + * @brief Get the scope. + * + * @return The scope identifier used for this secure storage instance + */ + std::string GetScope() const; + + /** + * @brief Check if secure storage is available on this platform. + * + * @return true if platform supports secure storage, false otherwise + */ + static bool IsAvailable(); + + // Prevent copying and moving + SecureStorage(const SecureStorage&) = delete; + SecureStorage& operator=(const SecureStorage&) = delete; + SecureStorage(SecureStorage&&) = delete; + SecureStorage& operator=(SecureStorage&&) = delete; + + private: + class Impl; + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/shortcut.cpp b/packages/cnativeapi/cxx_impl/src/shortcut.cpp new file mode 100644 index 0000000..885b36d --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/shortcut.cpp @@ -0,0 +1,65 @@ +#include "shortcut.h" + +namespace nativeapi { + +Shortcut::Shortcut(ShortcutId id, const ShortcutOptions& options) + : id_(id), + accelerator_(options.accelerator), + description_(options.description), + scope_(options.scope), + enabled_(options.enabled), + callback_(options.callback) {} + +Shortcut::Shortcut(ShortcutId id, const std::string& accelerator, std::function callback) + : id_(id), + accelerator_(accelerator), + description_(""), + scope_(ShortcutScope::Global), + enabled_(true), + callback_(callback) {} + +Shortcut::~Shortcut() = default; + +ShortcutId Shortcut::GetId() const { + return id_; +} + +std::string Shortcut::GetAccelerator() const { + return accelerator_; +} + +std::string Shortcut::GetDescription() const { + return description_; +} + +void Shortcut::SetDescription(const std::string& description) { + description_ = description; +} + +ShortcutScope Shortcut::GetScope() const { + return scope_; +} + +void Shortcut::SetEnabled(bool enabled) { + enabled_ = enabled; +} + +bool Shortcut::IsEnabled() const { + return enabled_; +} + +void Shortcut::Invoke() { + if (enabled_ && callback_) { + callback_(); + } +} + +void Shortcut::SetCallback(std::function callback) { + callback_ = callback; +} + +std::function Shortcut::GetCallback() const { + return callback_; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/shortcut.h b/packages/cnativeapi/cxx_impl/src/shortcut.h new file mode 100644 index 0000000..b4a274f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/shortcut.h @@ -0,0 +1,553 @@ +#pragma once + +#include +#include +#include + +#include "foundation/event.h" +#include "foundation/id_allocator.h" + +namespace nativeapi { + +typedef IdAllocator::IdType ShortcutId; + +/** + * @brief Defines the scope of a keyboard shortcut. + * + * This enum specifies whether a shortcut is active globally (system-wide) + * or only when the application has focus (application-local). + */ +enum class ShortcutScope { + /** + * @brief Global shortcut that works system-wide. + * + * The shortcut will be triggered regardless of which application has focus. + * This requires appropriate system permissions on some platforms. + */ + Global, + + /** + * @brief Application-local shortcut. + * + * The shortcut will only be triggered when the application has focus. + * This is less intrusive and doesn't require special permissions. + */ + Application +}; + +/** + * @brief Configuration options for creating a keyboard shortcut. + * + * This structure contains all the parameters needed to register a new + * keyboard shortcut, including the key combination, callback function, + * and optional metadata. + */ +struct ShortcutOptions { + /** + * @brief The keyboard shortcut string (e.g., "Ctrl+Shift+A"). + * + * Follows Electron-style accelerator format: + * - Modifiers: Ctrl, Alt, Shift, Cmd (macOS), Super (Linux), Meta + * - Keys: A-Z, 0-9, F1-F12, Space, Tab, Enter, Escape, etc. + * - Examples: "Ctrl+C", "Cmd+Shift+4", "Alt+F4" + */ + std::string accelerator; + + /** + * @brief Function to call when the shortcut is activated. + * + * This callback will be invoked on the main thread when the user + * presses the registered key combination. + */ + std::function callback; + + /** + * @brief Optional human-readable description of the shortcut's purpose. + * + * This can be used for displaying shortcut lists or help documentation. + */ + std::string description; + + /** + * @brief The scope of the shortcut (global or application-local). + * + * Defaults to Global for system-wide shortcuts. + */ + ShortcutScope scope = ShortcutScope::Global; + + /** + * @brief Whether the shortcut is initially enabled. + * + * Defaults to true. Disabled shortcuts remain registered but won't + * trigger their callbacks until enabled. + */ + bool enabled = true; +}; + +/** + * @brief Shortcut represents a registered keyboard shortcut. + * + * This class encapsulates a keyboard shortcut registration, including + * its key combination, callback function, and metadata. Shortcut instances + * are created and managed by the ShortcutManager. + * + * Key features: + * - Unique identifier for each shortcut + * - Enable/disable functionality without unregistering + * - Access to shortcut metadata (accelerator, description, scope) + * - Callback function management + * + * @note Shortcut instances should be created through ShortcutManager::Register() + * rather than directly constructed. + * @note This class is not thread-safe. All operations should be performed + * on the main thread or properly synchronized. + * + * @example + * ```cpp + * auto& manager = ShortcutManager::GetInstance(); + * auto shortcut = manager.Register("Ctrl+Shift+Q", []() { + * std::cout << "Quick action triggered!" << std::endl; + * }); + * + * // Temporarily disable + * shortcut->SetEnabled(false); + * + * // Check properties + * std::cout << "Accelerator: " << shortcut->GetAccelerator() << std::endl; + * std::cout << "Scope: " << (shortcut->GetScope() == ShortcutScope::Global ? "Global" : + * "Application") << std::endl; + * + * // Re-enable + * shortcut->SetEnabled(true); + * ``` + */ +class Shortcut { + public: + /** + * @brief Constructor for creating a shortcut with detailed options. + * + * Creates a new shortcut instance with the specified configuration. + * This constructor is typically called by ShortcutManager::Register(). + * + * @param id Unique identifier for this shortcut + * @param options Configuration options for the shortcut + */ + Shortcut(ShortcutId id, const ShortcutOptions& options); + + /** + * @brief Constructor for creating a shortcut with basic parameters. + * + * Creates a new global shortcut with the specified accelerator and callback. + * This is a convenience constructor for simple use cases. + * + * @param id Unique identifier for this shortcut + * @param accelerator The keyboard shortcut string + * @param callback Function to call when activated + */ + Shortcut(ShortcutId id, const std::string& accelerator, std::function callback); + + // Shortcut is an identity object: it is managed + // through std::shared_ptr by the ShortcutManager and identified by its + // ShortcutId, so it is not copyable. Share the std::shared_ptr instead. + Shortcut(const Shortcut&) = delete; + Shortcut& operator=(const Shortcut&) = delete; + Shortcut(Shortcut&&) = delete; + Shortcut& operator=(Shortcut&&) = delete; + + /** + * @brief Virtual destructor for proper cleanup. + * + * Ensures proper cleanup of resources when the shortcut is destroyed. + */ + virtual ~Shortcut(); + + /** + * @brief Get the unique identifier of this shortcut. + * + * @return The shortcut's unique ID + */ + ShortcutId GetId() const; + + /** + * @brief Get the keyboard accelerator string. + * + * Returns the key combination string used to trigger this shortcut, + * such as "Ctrl+Shift+A" or "Cmd+Space". + * + * @return The accelerator string + */ + std::string GetAccelerator() const; + + /** + * @brief Get the human-readable description of this shortcut. + * + * Returns the description provided when the shortcut was created, + * or an empty string if no description was set. + * + * @return The shortcut description + */ + std::string GetDescription() const; + + /** + * @brief Set a new description for this shortcut. + * + * Updates the human-readable description. This doesn't affect the + * shortcut's functionality, only its metadata. + * + * @param description The new description text + */ + void SetDescription(const std::string& description); + + /** + * @brief Get the scope of this shortcut. + * + * Returns whether this is a global (system-wide) or application-local + * shortcut. + * + * @return The shortcut scope + */ + ShortcutScope GetScope() const; + + /** + * @brief Enable or disable this shortcut. + * + * When disabled, the shortcut remains registered but won't trigger + * its callback. This is useful for temporarily disabling shortcuts + * without unregistering them. + * + * @param enabled true to enable, false to disable + * + * @example + * ```cpp + * // Disable during modal dialog + * shortcut->SetEnabled(false); + * ShowModalDialog(); + * shortcut->SetEnabled(true); + * ``` + */ + void SetEnabled(bool enabled); + + /** + * @brief Check if this shortcut is currently enabled. + * + * @return true if enabled, false if disabled + */ + bool IsEnabled() const; + + /** + * @brief Invoke the shortcut's callback function. + * + * Manually triggers the shortcut's callback. This is primarily used + * internally by the ShortcutManager when the key combination is pressed, + * but can also be called programmatically for testing or automation. + * + * @note This method respects the enabled state - it won't invoke the + * callback if the shortcut is disabled. + * + * @example + * ```cpp + * // Programmatically trigger shortcut + * if (shortcut->IsEnabled()) { + * shortcut->Invoke(); + * } + * ``` + */ + void Invoke(); + + /** + * @brief Set a new callback function for this shortcut. + * + * Replaces the current callback with a new one. This allows changing + * the shortcut's behavior without unregistering and re-registering it. + * + * @param callback The new callback function + * + * @example + * ```cpp + * // Change behavior dynamically + * shortcut->SetCallback([]() { + * std::cout << "New behavior!" << std::endl; + * }); + * ``` + */ + void SetCallback(std::function callback); + + /** + * @brief Get the current callback function. + * + * Returns a copy of the callback function. This is primarily useful + * for testing or introspection. + * + * @return The callback function + */ + std::function GetCallback() const; + + private: + /** + * @brief Unique identifier for this shortcut. + */ + ShortcutId id_; + + /** + * @brief The keyboard accelerator string (e.g., "Ctrl+Shift+A"). + */ + std::string accelerator_; + + /** + * @brief Human-readable description of the shortcut's purpose. + */ + std::string description_; + + /** + * @brief The scope of the shortcut (global or application-local). + */ + ShortcutScope scope_; + + /** + * @brief Whether the shortcut is currently enabled. + */ + bool enabled_; + + /** + * @brief The callback function to invoke when the shortcut is triggered. + */ + std::function callback_; +}; + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * @brief Base class for all shortcut-related events. + * + * This class provides common functionality for shortcut events, + * including access to the shortcut ID and accelerator string that + * triggered the event. + */ +class ShortcutEvent : public Event { + public: + /** + * @brief Constructor for ShortcutEvent. + * + * @param shortcut_id The unique ID of the shortcut that triggered this event + * @param accelerator The keyboard accelerator string (e.g., "Ctrl+Shift+A") + */ + explicit ShortcutEvent(ShortcutId shortcut_id, const std::string& accelerator) + : shortcut_id_(shortcut_id), accelerator_(accelerator) {} + + /** + * @brief Virtual destructor. + */ + virtual ~ShortcutEvent() = default; + + /** + * @brief Get the shortcut ID associated with this event. + * + * @return The unique identifier of the shortcut + */ + ShortcutId GetShortcutId() const { return shortcut_id_; } + + /** + * @brief Get the accelerator string associated with this event. + * + * Returns the keyboard shortcut string that was pressed, + * such as "Ctrl+Shift+A" or "Cmd+Space". + * + * @return The accelerator string + */ + std::string GetAccelerator() const { return accelerator_; } + + /** + * @brief Get a string representation of the event type (for debugging). + * + * Default implementation returns "ShortcutEvent". + * + * @return The event type name + */ + std::string GetTypeName() const override { return "ShortcutEvent"; } + + private: + /** + * @brief The unique ID of the shortcut. + */ + ShortcutId shortcut_id_; + + /** + * @brief The keyboard accelerator string. + */ + std::string accelerator_; +}; + +/** + * @brief Event emitted when a keyboard shortcut is activated. + * + * This event is emitted when a registered keyboard shortcut is triggered + * by the user pressing the corresponding key combination. The event is + * emitted before the shortcut's callback is invoked, allowing listeners + * to perform additional actions or logging. + * + * @example + * ```cpp + * auto& manager = ShortcutManager::GetInstance(); + * + * // Listen for shortcut activations + * manager.AddListener([](const ShortcutActivatedEvent& event) { + * std::cout << "Shortcut activated: " << event.GetAccelerator() << std::endl; + * std::cout << "Shortcut ID: " << event.GetShortcutId() << std::endl; + * }); + * + * // Register a shortcut + * auto shortcut = manager.Register("Ctrl+Shift+Q", []() { + * std::cout << "Quick action!" << std::endl; + * }); + * ``` + */ +class ShortcutActivatedEvent : public ShortcutEvent { + public: + /** + * @brief Constructor for ShortcutActivatedEvent. + * + * @param shortcut_id The unique ID of the activated shortcut + * @param accelerator The keyboard accelerator string + */ + explicit ShortcutActivatedEvent(ShortcutId shortcut_id, const std::string& accelerator) + : ShortcutEvent(shortcut_id, accelerator) {} + + /** + * @brief Get a string representation of the event type. + * + * @return "ShortcutActivatedEvent" + */ + std::string GetTypeName() const override { return "ShortcutActivatedEvent"; } +}; + +/** + * @brief Event emitted when a keyboard shortcut is successfully registered. + * + * This event is emitted by the ShortcutManager when a new shortcut is + * successfully registered with the system. It allows listeners to track + * which shortcuts are active and perform any necessary setup. + * + * @example + * ```cpp + * auto& manager = ShortcutManager::GetInstance(); + * + * // Listen for shortcut registrations + * manager.AddListener([](const ShortcutRegisteredEvent& event) { + * std::cout << "Shortcut registered: " << event.GetAccelerator() << std::endl; + * // Update UI to show available shortcuts + * }); + * ``` + */ +class ShortcutRegisteredEvent : public ShortcutEvent { + public: + /** + * @brief Constructor for ShortcutRegisteredEvent. + * + * @param shortcut_id The unique ID of the registered shortcut + * @param accelerator The keyboard accelerator string + */ + explicit ShortcutRegisteredEvent(ShortcutId shortcut_id, const std::string& accelerator) + : ShortcutEvent(shortcut_id, accelerator) {} + + /** + * @brief Get a string representation of the event type. + * + * @return "ShortcutRegisteredEvent" + */ + std::string GetTypeName() const override { return "ShortcutRegisteredEvent"; } +}; + +/** + * @brief Event emitted when a keyboard shortcut is unregistered. + * + * This event is emitted by the ShortcutManager when a shortcut is + * unregistered and removed from the system. It allows listeners to + * track shortcut lifecycle and perform cleanup. + * + * @example + * ```cpp + * auto& manager = ShortcutManager::GetInstance(); + * + * // Listen for shortcut unregistrations + * manager.AddListener([](const ShortcutUnregisteredEvent& event) { + * std::cout << "Shortcut unregistered: " << event.GetAccelerator() << std::endl; + * // Update UI to remove shortcut from list + * }); + * ``` + */ +class ShortcutUnregisteredEvent : public ShortcutEvent { + public: + /** + * @brief Constructor for ShortcutUnregisteredEvent. + * + * @param shortcut_id The unique ID of the unregistered shortcut + * @param accelerator The keyboard accelerator string + */ + explicit ShortcutUnregisteredEvent(ShortcutId shortcut_id, const std::string& accelerator) + : ShortcutEvent(shortcut_id, accelerator) {} + + /** + * @brief Get a string representation of the event type. + * + * @return "ShortcutUnregisteredEvent" + */ + std::string GetTypeName() const override { return "ShortcutUnregisteredEvent"; } +}; + +/** + * @brief Event emitted when a shortcut registration fails. + * + * This event is emitted when the system fails to register a keyboard + * shortcut, typically due to conflicts with existing shortcuts or + * system restrictions. The event includes an error message describing + * the failure reason. + * + * @example + * ```cpp + * auto& manager = ShortcutManager::GetInstance(); + * + * // Listen for registration failures + * manager.AddListener([](const ShortcutRegistrationFailedEvent& + * event) { std::cerr << "Failed to register shortcut: " << event.GetAccelerator() << std::endl; + * std::cerr << "Reason: " << event.GetErrorMessage() << std::endl; + * }); + * ``` + */ +class ShortcutRegistrationFailedEvent : public ShortcutEvent { + public: + /** + * @brief Constructor for ShortcutRegistrationFailedEvent. + * + * @param shortcut_id The unique ID that was assigned to the shortcut (may be 0 if not assigned) + * @param accelerator The keyboard accelerator string that failed to register + * @param error_message Description of why the registration failed + */ + explicit ShortcutRegistrationFailedEvent(ShortcutId shortcut_id, + const std::string& accelerator, + const std::string& error_message) + : ShortcutEvent(shortcut_id, accelerator), error_message_(error_message) {} + + /** + * @brief Get the error message describing why registration failed. + * + * @return The error message + */ + std::string GetErrorMessage() const { return error_message_; } + + /** + * @brief Get a string representation of the event type. + * + * @return "ShortcutRegistrationFailedEvent" + */ + std::string GetTypeName() const override { return "ShortcutRegistrationFailedEvent"; } + + private: + /** + * @brief Description of the registration failure. + */ + std::string error_message_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/shortcut_manager.cpp b/packages/cnativeapi/cxx_impl/src/shortcut_manager.cpp new file mode 100644 index 0000000..0f67d45 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/shortcut_manager.cpp @@ -0,0 +1,254 @@ +#include "shortcut_manager.h" + +#include + +namespace nativeapi { + +// Singleton instance +ShortcutManager& ShortcutManager::GetInstance() { + static ShortcutManager instance; + return instance; +} + +// Check if global shortcuts are supported +bool ShortcutManager::IsSupported() { + return pimpl_->IsSupported(); +} + +// Register a new keyboard shortcut with callback +std::shared_ptr ShortcutManager::Register(const std::string& accelerator, + std::function callback) { + ShortcutOptions options; + options.accelerator = accelerator; + options.callback = callback; + return Register(options); +} + +// Register a new keyboard shortcut with options +std::shared_ptr ShortcutManager::Register(const ShortcutOptions& options) { + std::unique_lock lock(mutex_); + + // Validate accelerator format + if (!IsValidAccelerator(options.accelerator)) { + // Emit failure event + EmitAsync(0, options.accelerator, + "Invalid accelerator format"); + return nullptr; + } + + // Check if accelerator is already registered. + // NOTE: must use the *Unlocked variant — we already hold mutex_, and the + // check must stay atomic with the insert below. + if (!IsAvailableUnlocked(options.accelerator)) { + // Emit failure event + EmitAsync(0, options.accelerator, + "Accelerator already registered"); + return nullptr; + } + + // Allocate new ID + ShortcutId id = next_shortcut_id_++; + + // Create shortcut instance + auto shortcut = std::make_shared(id, options); + + // Register with platform + if (!pimpl_->RegisterShortcut(shortcut)) { + // Platform registration failed + EmitAsync(id, options.accelerator, + "Platform registration failed"); + return nullptr; + } + + // Store in registries + shortcuts_by_id_[id] = shortcut; + shortcuts_by_accelerator_[options.accelerator] = shortcut; + + // Emit success event + EmitAsync(id, options.accelerator); + + return shortcut; +} + +// Unregister a shortcut by ID +bool ShortcutManager::Unregister(ShortcutId id) { + std::unique_lock lock(mutex_); + + auto it = shortcuts_by_id_.find(id); + if (it == shortcuts_by_id_.end()) { + return false; + } + + auto shortcut = it->second; + std::string accelerator = shortcut->GetAccelerator(); + + // Unregister from platform + pimpl_->UnregisterShortcut(shortcut); + + // Remove from registries + shortcuts_by_id_.erase(it); + shortcuts_by_accelerator_.erase(accelerator); + + // Emit event + EmitAsync(id, accelerator); + + return true; +} + +// Unregister a shortcut by accelerator +bool ShortcutManager::Unregister(const std::string& accelerator) { + std::unique_lock lock(mutex_); + + auto it = shortcuts_by_accelerator_.find(accelerator); + if (it == shortcuts_by_accelerator_.end()) { + return false; + } + + ShortcutId id = it->second->GetId(); + + lock.unlock(); + bool result = Unregister(id); + lock.lock(); + + return result; +} + +// Unregister all shortcuts +int ShortcutManager::UnregisterAll() { + std::unique_lock lock(mutex_); + + int count = 0; + + // Create a copy of IDs to avoid iterator invalidation + std::vector ids; + ids.reserve(shortcuts_by_id_.size()); + for (const auto& [id, shortcut] : shortcuts_by_id_) { + ids.push_back(id); + } + + lock.unlock(); + for (ShortcutId id : ids) { + if (Unregister(id)) { + count++; + } + } + lock.lock(); + + return count; +} + +// Get a shortcut by ID +std::shared_ptr ShortcutManager::Get(ShortcutId id) { + std::lock_guard lock(mutex_); + + auto it = shortcuts_by_id_.find(id); + return (it != shortcuts_by_id_.end()) ? it->second : nullptr; +} + +// Get a shortcut by accelerator +std::shared_ptr ShortcutManager::Get(const std::string& accelerator) { + std::lock_guard lock(mutex_); + + auto it = shortcuts_by_accelerator_.find(accelerator); + return (it != shortcuts_by_accelerator_.end()) ? it->second : nullptr; +} + +// Get all shortcuts +std::vector> ShortcutManager::GetAll() { + std::lock_guard lock(mutex_); + + std::vector> result; + result.reserve(shortcuts_by_id_.size()); + + for (const auto& [id, shortcut] : shortcuts_by_id_) { + result.push_back(shortcut); + } + + return result; +} + +// Get shortcuts by scope +std::vector> ShortcutManager::GetByScope(ShortcutScope scope) { + std::lock_guard lock(mutex_); + + std::vector> result; + + for (const auto& [id, shortcut] : shortcuts_by_id_) { + if (shortcut->GetScope() == scope) { + result.push_back(shortcut); + } + } + + return result; +} + +// Check if an accelerator is available +bool ShortcutManager::IsAvailable(const std::string& accelerator) { + std::lock_guard lock(mutex_); + return IsAvailableUnlocked(accelerator); +} + +// Availability check for callers that already hold mutex_. +bool ShortcutManager::IsAvailableUnlocked(const std::string& accelerator) const { + return shortcuts_by_accelerator_.find(accelerator) == shortcuts_by_accelerator_.end(); +} + +// Validate accelerator format +bool ShortcutManager::IsValidAccelerator(const std::string& accelerator) { + if (accelerator.empty()) { + return false; + } + + // Basic validation using regex. + // Format: [Modifier+]*Key + // + // Modifiers: Ctrl/Control, Alt/Option, Shift, Cmd/Command, Super, Meta, + // CmdOrCtrl/CommandOrControl + // Keys: A-Z, 0-9, F1-F24, named keys (Space, Enter, PageUp, Comma, ...), + // the keypad (Num0-Num9, NumAdd, ...), and the literal punctuation + // characters those names stand for (",", ".", "/", ...). + // + // This has to stay in step with the per-platform token tables in + // src/platform/*/shortcut_manager_*. Anything accepted here but unknown to a + // platform parser degrades to a ShortcutRegistrationFailedEvent rather than a + // silent no-op, but the two lists are meant to agree. + static const std::regex accelerator_regex( + R"(^(?:(?:Ctrl|Control|Alt|Option|Shift|Cmd|Command|Super|Meta|CmdOrCtrl|CommandOrControl)\+)*)" + R"((?:F1[0-9]|F2[0-4]|F[1-9]|Num[0-9]|NumDec|NumAdd|NumSub|NumMult|NumDiv|NumEnter)" + R"(|Space|Tab|Enter|Return|Escape|Esc|Backspace|ForwardDelete|Delete|Insert|Help)" + R"(|Home|End|PageUp|PageDown|Up|Down|Left|Right)" + R"(|Plus|Minus|Equal|Comma|Period|Slash|Backslash|Semicolon|Quote)" + R"(|LeftBracket|RightBracket|Grave|Backquote)" + R"(|[A-Za-z0-9]|[,./\\;'\[\]`=\-])$)", + std::regex::icase); + + return std::regex_match(accelerator, accelerator_regex); +} + +// Enable or disable shortcut processing +void ShortcutManager::SetEnabled(bool enabled) { + std::lock_guard lock(mutex_); + enabled_ = enabled; +} + +// Check if shortcut processing is enabled +bool ShortcutManager::IsEnabled() const { + std::lock_guard lock(mutex_); + return enabled_; +} + +void ShortcutManager::EmitShortcutActivated(ShortcutId id, const std::string& accelerator) { + EmitAsync(id, accelerator); +} + +// Start event listening (called when first listener is added) +void ShortcutManager::StartEventListening() { + pimpl_->SetupEventMonitoring(); +} + +// Stop event listening (called when last listener is removed) +void ShortcutManager::StopEventListening() { + pimpl_->CleanupEventMonitoring(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/shortcut_manager.h b/packages/cnativeapi/cxx_impl/src/shortcut_manager.h new file mode 100644 index 0000000..9a37c77 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/shortcut_manager.h @@ -0,0 +1,438 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "foundation/event_emitter.h" +#include "foundation/id_allocator.h" +#include "shortcut.h" + +namespace nativeapi { + +typedef IdAllocator::IdType ShortcutId; + +/** + * @brief ShortcutManager is a singleton class that manages global keyboard shortcuts. + * + * The ShortcutManager provides centralized access to system-wide keyboard shortcut + * registration and handling. It follows the singleton pattern to ensure there's only + * one instance managing all shortcuts throughout the application lifetime. + * + * Key features: + * - Singleton pattern ensures centralized shortcut management + * - Event-driven architecture for shortcut activation notifications + * - Cross-platform shortcut registration and monitoring + * - Thread-safe access to the singleton instance + * - Automatic cleanup of resources on destruction + * - Support for both global and application-local shortcuts + * + * @note This class is thread-safe for singleton access and shortcut operations. + * @note Shortcut instances should be created and managed through this manager. + */ +class ShortcutManager : public EventEmitter { + public: + /** + * @brief Get the singleton instance of ShortcutManager. + * + * This method provides access to the unique instance of ShortcutManager using + * the Meyer's singleton pattern. The instance is created on first call and + * remains alive for the duration of the application. This method is thread-safe + * and guarantees that only one instance will be created even in multi-threaded + * environments. + * + * @return Reference to the singleton ShortcutManager instance + * @thread_safety This method is thread-safe + * + * @code + * // Usage example: + * auto& manager = ShortcutManager::GetInstance(); + * auto shortcut = manager.Register("Ctrl+Shift+A", callback); + * @endcode + */ + static ShortcutManager& GetInstance(); + + /** + * @brief Destructor for ShortcutManager. + * + * Cleans up all managed shortcuts, unregisters them from the system, + * and releases system resources. This is automatically called when + * the application terminates. + */ + virtual ~ShortcutManager(); + + /** + * @brief Check if global shortcuts are supported on the current platform. + * + * Some platforms or configurations may not support global keyboard shortcuts + * due to security restrictions or desktop environment limitations. This method + * allows checking for availability before attempting to register shortcuts. + * + * @return true if global shortcuts are supported, false otherwise + */ + bool IsSupported(); + + /** + * @brief Register a new global keyboard shortcut. + * + * Creates and registers a new keyboard shortcut that can be triggered + * system-wide, regardless of which application has focus. The shortcut + * will trigger the provided callback when activated. + * + * @param accelerator The keyboard shortcut string (e.g., "Ctrl+Shift+A", "Cmd+Space") + * @param callback Function to call when the shortcut is activated + * @return Shared pointer to the created Shortcut instance, nullptr if registration failed + * @thread_safety This method is thread-safe + * + * @note Accelerator format follows Electron-style conventions: + * - Modifiers: Ctrl, Alt, Shift, Cmd (macOS), Super (Linux), Meta + * - Keys: A-Z, 0-9, F1-F12, Space, Tab, Enter, Escape, etc. + * - Examples: "Ctrl+C", "Cmd+Shift+4", "Alt+F4", "Ctrl+Alt+Delete" + * + * @example + * ```cpp + * auto shortcut = manager.Register("Ctrl+Shift+Q", []() { + * std::cout << "Quick action triggered!" << std::endl; + * }); + * + * if (!shortcut) { + * std::cerr << "Failed to register shortcut" << std::endl; + * } + * ``` + */ + std::shared_ptr Register(const std::string& accelerator, + std::function callback); + + /** + * @brief Register a new keyboard shortcut with detailed options. + * + * Creates and registers a keyboard shortcut with additional configuration + * options such as scope (global vs application-local) and description. + * + * @param options ShortcutOptions struct containing shortcut configuration + * @return Shared pointer to the created Shortcut instance, nullptr if registration failed + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * ShortcutOptions options; + * options.accelerator = "Ctrl+Alt+T"; + * options.callback = []() { OpenTerminal(); }; + * options.description = "Open terminal"; + * options.scope = ShortcutScope::Global; + * + * auto shortcut = manager.Register(options); + * ``` + */ + std::shared_ptr Register(const ShortcutOptions& options); + + /** + * @brief Unregister a keyboard shortcut by its ID. + * + * Removes a previously registered shortcut from the system and + * stops monitoring for its activation. + * + * @param id The unique identifier of the shortcut to unregister + * @return true if the shortcut was successfully unregistered, false otherwise + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * ShortcutId id = shortcut->GetId(); + * bool success = manager.Unregister(id); + * ``` + */ + bool Unregister(ShortcutId id); + + /** + * @brief Unregister a keyboard shortcut by its accelerator string. + * + * Removes a previously registered shortcut from the system using + * its accelerator string identifier. + * + * @param accelerator The keyboard shortcut string to unregister + * @return true if the shortcut was successfully unregistered, false otherwise + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * bool success = manager.Unregister("Ctrl+Shift+Q"); + * ``` + */ + bool Unregister(const std::string& accelerator); + + /** + * @brief Unregister all keyboard shortcuts. + * + * Removes all currently registered shortcuts from the system. + * This is useful for cleanup or when switching shortcut profiles. + * + * @return Number of shortcuts that were unregistered + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * int count = manager.UnregisterAll(); + * std::cout << "Unregistered " << count << " shortcuts" << std::endl; + * ``` + */ + int UnregisterAll(); + + /** + * @brief Get a shortcut by its unique ID. + * + * Retrieves a previously registered shortcut using its assigned ID. + * + * @param id The unique identifier of the shortcut + * @return Shared pointer to the Shortcut if found, nullptr otherwise + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * auto shortcut = manager.Get(shortcut_id); + * if (shortcut) { + * std::cout << "Found shortcut: " << shortcut->GetAccelerator() << std::endl; + * } + * ``` + */ + std::shared_ptr Get(ShortcutId id); + + /** + * @brief Get a shortcut by its accelerator string. + * + * Retrieves a previously registered shortcut using its accelerator string. + * + * @param accelerator The keyboard shortcut string to find + * @return Shared pointer to the Shortcut if found, nullptr otherwise + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * auto shortcut = manager.Get("Ctrl+Shift+Q"); + * if (shortcut) { + * shortcut->SetEnabled(false); + * } + * ``` + */ + std::shared_ptr Get(const std::string& accelerator); + + /** + * @brief Get all managed shortcuts. + * + * Returns a vector containing all currently registered shortcut instances. + * The returned vector is a snapshot of the current state and modifications + * to it won't affect the internal shortcut registry. + * + * @return Vector of shared pointers to all Shortcut instances + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * auto all_shortcuts = manager.GetAll(); + * for (auto& shortcut : all_shortcuts) { + * std::cout << shortcut->GetAccelerator() << std::endl; + * } + * ``` + */ + std::vector> GetAll(); + + /** + * @brief Get shortcuts filtered by scope. + * + * Returns shortcuts that match the specified scope (global or application-local). + * + * @param scope The shortcut scope to filter by + * @return Vector of shared pointers to matching Shortcut instances + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * auto global_shortcuts = manager.GetByScope(ShortcutScope::Global); + * auto app_shortcuts = manager.GetByScope(ShortcutScope::Application); + * ``` + */ + std::vector> GetByScope(ShortcutScope scope); + + /** + * @brief Check if a specific accelerator is available for registration. + * + * Determines whether a keyboard shortcut string is available for use, + * i.e., not already registered by this application or conflicting with + * system shortcuts. + * + * @param accelerator The keyboard shortcut string to check + * @return true if the accelerator is available, false if already in use + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * if (manager.IsAvailable("Ctrl+Shift+N")) { + * auto shortcut = manager.Register("Ctrl+Shift+N", callback); + * } else { + * std::cout << "Shortcut already in use" << std::endl; + * } + * ``` + */ + bool IsAvailable(const std::string& accelerator); + + /** + * @brief Validate an accelerator string format. + * + * Checks if the provided accelerator string follows the correct format + * and contains valid key combinations. This is useful for validating + * user input before attempting registration. + * + * @param accelerator The keyboard shortcut string to validate + * @return true if the format is valid, false otherwise + * + * @example + * ```cpp + * if (manager.IsValidAccelerator("Ctrl+Shift+Q")) { + * // Valid format, safe to register + * } else { + * std::cout << "Invalid shortcut format" << std::endl; + * } + * ``` + */ + bool IsValidAccelerator(const std::string& accelerator); + + /** + * @brief Enable or disable shortcut processing. + * + * Allows temporarily disabling all shortcut processing without unregistering + * shortcuts. When disabled, shortcuts will remain registered but won't trigger + * their callbacks. This is useful for modal dialogs or when the application + * needs to temporarily suppress shortcut handling. + * + * @param enabled true to enable shortcut processing, false to disable + * @thread_safety This method is thread-safe + * + * @example + * ```cpp + * // Disable shortcuts during modal dialog + * manager.SetEnabled(false); + * ShowModalDialog(); + * manager.SetEnabled(true); // Re-enable after dialog closes + * ``` + */ + void SetEnabled(bool enabled); + + /** + * @brief Check if shortcut processing is enabled. + * + * @return true if shortcut processing is enabled, false otherwise + * @thread_safety This method is thread-safe + */ + bool IsEnabled() const; + + /** + * @brief Emit a shortcut activated event (internal use). + * + * Platform implementations should call this when a registered shortcut fires. + */ + void EmitShortcutActivated(ShortcutId id, const std::string& accelerator); + + // Prevent copy construction and assignment to maintain singleton property + ShortcutManager(const ShortcutManager&) = delete; + ShortcutManager& operator=(const ShortcutManager&) = delete; + ShortcutManager(ShortcutManager&&) = delete; + ShortcutManager& operator=(ShortcutManager&&) = delete; + + /** + * @brief Private implementation class using the PIMPL idiom. + */ + class Impl { + public: + virtual ~Impl() = default; + virtual bool IsSupported() = 0; + virtual bool RegisterShortcut(const std::shared_ptr& shortcut) = 0; + virtual bool UnregisterShortcut(const std::shared_ptr& shortcut) = 0; + virtual void SetupEventMonitoring() = 0; + virtual void CleanupEventMonitoring() = 0; + }; + + protected: + /** + * @brief Called when the first listener is added. + * + * Starts platform-specific shortcut monitoring. This is called automatically + * by the EventEmitter when transitioning from 0 to 1+ listeners. + */ + void StartEventListening() override; + + /** + * @brief Called when the last listener is removed. + * + * Stops platform-specific shortcut monitoring. This is called automatically + * by the EventEmitter when transitioning from 1+ to 0 listeners. + */ + void StopEventListening() override; + + private: + /** + * @brief Private constructor to enforce singleton pattern. + * + * Initializes the ShortcutManager instance and sets up initial state. + */ + ShortcutManager(); + + /** + * @brief Pointer to the private implementation instance. + */ + std::unique_ptr pimpl_; + + /** + * @brief Container for storing active shortcut instances by ID. + * + * Maps shortcut IDs to their corresponding Shortcut instances + * for efficient lookup and management. + */ + std::unordered_map> shortcuts_by_id_; + + /** + * @brief Container for storing active shortcut instances by accelerator. + * + * Maps accelerator strings to their corresponding Shortcut instances + * for efficient lookup by keyboard combination. + */ + std::unordered_map> shortcuts_by_accelerator_; + + /** + * @brief ID generator for creating unique shortcut identifiers. + * + * Maintains the next available ID to assign to newly created shortcuts. + * This ensures each shortcut has a unique identifier. + */ + ShortcutId next_shortcut_id_; + + /** + * @brief Flag indicating whether shortcut processing is enabled. + */ + bool enabled_; + + /** + * @brief Mutex for thread-safe operations. + * + * Protects access to internal data structures to ensure thread safety + * when multiple threads access the ShortcutManager simultaneously. + */ + mutable std::mutex mutex_; + + /** + * @brief Availability check for callers that already hold mutex_. + * + * The public IsAvailable() acquires mutex_, so calling it from an already- + * locked section self-deadlocks (mutex_ is not recursive). Register() needs + * the check and the subsequent insert to be one atomic step, so it must not + * drop the lock in between — it uses this helper instead. + * + * @param accelerator The accelerator string to check. + * @return true if no shortcut is currently registered for this accelerator. + */ + bool IsAvailableUnlocked(const std::string& accelerator) const; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/storage.h b/packages/cnativeapi/cxx_impl/src/storage.h new file mode 100644 index 0000000..227e387 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/storage.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +namespace nativeapi { + +/** + * @brief Abstract interface for key-value storage, similar to Web Storage API. + * + * This interface provides a simple key-value storage mechanism with support + * for string keys and values. Implementations can provide different storage + * backends (preferences, secure storage, etc.). + */ +class Storage { + public: + virtual ~Storage() = default; + + /** + * @brief Set a key-value pair. + * + * @param key The key to set + * @param value The value to store + * @return true if successful, false otherwise + */ + virtual bool Set(const std::string& key, const std::string& value) = 0; + + /** + * @brief Get the value for a given key. + * + * @param key The key to retrieve + * @param default_value Default value if key doesn't exist + * @return The stored value or default_value if not found + */ + virtual std::string Get(const std::string& key, const std::string& default_value = "") const = 0; + + /** + * @brief Remove a key-value pair. + * + * @param key The key to remove + * @return true if successful, false if key doesn't exist + */ + virtual bool Remove(const std::string& key) = 0; + + /** + * @brief Clear all key-value pairs. + * + * @return true if successful, false otherwise + */ + virtual bool Clear() = 0; + + /** + * @brief Check if a key exists. + * + * @param key The key to check + * @return true if key exists, false otherwise + */ + virtual bool Contains(const std::string& key) const = 0; + + /** + * @brief Get all keys. + * + * @return Vector of all keys in storage + */ + virtual std::vector GetKeys() const = 0; + + /** + * @brief Get the number of stored items. + * + * @return Number of key-value pairs + */ + virtual size_t GetSize() const = 0; + + /** + * @brief Get all key-value pairs. + * + * @return Map of all key-value pairs + */ + virtual std::map GetAll() const = 0; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/tray_icon.h b/packages/cnativeapi/cxx_impl/src/tray_icon.h new file mode 100644 index 0000000..7115521 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/tray_icon.h @@ -0,0 +1,502 @@ +#pragma once + +#include +#include +#include +#include +#include "foundation/event.h" +#include "foundation/event_emitter.h" +#include "foundation/geometry.h" +#include "foundation/id_allocator.h" +#include "menu.h" + +namespace nativeapi { + +class Image; + +typedef IdAllocator::IdType TrayIconId; + +/** + * @brief Defines how the context menu is triggered for a tray icon. + * + * This enum specifies which mouse interactions should display the tray icon's + * context menu. The values align with tray icon event types for consistency. + */ +enum class ContextMenuTrigger { + /** + * @brief Context menu is not automatically triggered by mouse events. + * + * The application must call OpenContextMenu() explicitly to display the menu. + * Use this when you want full control over when the menu appears. + */ + None, + + /** + * @brief Context menu is triggered on TrayIconClickedEvent. + * + * Automatically opens the context menu when the tray icon is left-clicked. + * This is common on some Linux desktop environments. + */ + Clicked, + + /** + * @brief Context menu is triggered on TrayIconRightClickedEvent. + * + * Automatically opens the context menu when the tray icon is right-clicked. + * This follows the convention on Windows and most desktop environments. + */ + RightClicked, + + /** + * @brief Context menu is triggered on TrayIconDoubleClickedEvent. + * + * Automatically opens the context menu when the tray icon is double-clicked. + * Less common but useful for applications that use single-click for another action. + */ + DoubleClicked +}; + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * @brief Base class for all tray icon-related events. + * + * This class provides common functionality for tray icon events. + */ +class TrayIconEvent : public Event { + public: + virtual ~TrayIconEvent() = default; + + std::string GetTypeName() const override { return "TrayIconEvent"; } +}; + +/** + * @brief Tray icon clicked event. + * + * This event is fired when a tray icon is clicked (left-clicked). + */ +class TrayIconClickedEvent : public TrayIconEvent { + public: + TrayIconClickedEvent(TrayIconId tray_icon_id) : tray_icon_id_(tray_icon_id) {} + + TrayIconId GetTrayIconId() const { return tray_icon_id_; } + + std::string GetTypeName() const override { return "TrayIconClickedEvent"; } + + private: + TrayIconId tray_icon_id_; +}; + +/** + * @brief Tray icon right-clicked event. + * + * This event is fired when a tray icon is right-clicked. + */ +class TrayIconRightClickedEvent : public TrayIconEvent { + public: + TrayIconRightClickedEvent(TrayIconId tray_icon_id) : tray_icon_id_(tray_icon_id) {} + + TrayIconId GetTrayIconId() const { return tray_icon_id_; } + + std::string GetTypeName() const override { return "TrayIconRightClickedEvent"; } + + private: + TrayIconId tray_icon_id_; +}; + +/** + * @brief Tray icon double-clicked event. + * + * This event is fired when a tray icon is double-clicked. + */ +class TrayIconDoubleClickedEvent : public TrayIconEvent { + public: + TrayIconDoubleClickedEvent(TrayIconId tray_icon_id) : tray_icon_id_(tray_icon_id) {} + + TrayIconId GetTrayIconId() const { return tray_icon_id_; } + + std::string GetTypeName() const override { return "TrayIconDoubleClickedEvent"; } + + private: + TrayIconId tray_icon_id_; +}; + +/** + * @brief TrayIcon represents a system tray icon (notification area icon). + * + * This class provides a cross-platform interface for creating and managing + * system tray icons. System tray icons appear in the notification area of + * the desktop and provide quick access to application functionality through + * context menus and click events. + * + * The class supports: + * - Setting custom icons (including base64-encoded images) + * - Displaying text titles and tooltips + * - Context menus for user interaction + * - Event emission for mouse clicks (TrayIconClickedEvent, + * TrayIconRightClickedEvent, TrayIconDoubleClickedEvent) + * - Visibility control + * + * @note This class uses the PIMPL idiom to hide platform-specific + * implementation details and ensure binary compatibility across different + * platforms. + * + * @example + * ```cpp + * // Create a tray icon + * auto tray_icon = std::make_shared(); + * tray_icon->SetIcon("path/to/icon.png"); + * tray_icon->SetTooltip("My Application"); + * + * // Set up event listeners + * tray_icon->AddListener([](const TrayIconClickedEvent& + * event) { + * // Handle left click - show/hide main window + * main_window->IsVisible() ? main_window->Hide() : main_window->Show(); + * }); + * + * tray_icon->AddListener([](const + * TrayIconRightClickedEvent& event) { + * // Handle right click - open context menu + * tray_icon->OpenContextMenu(); + * }); + * + * // Set up a context menu + * Menu menu; + * auto item = menu.CreateItem("Exit"); + * menu.AddItem(item); + * tray_icon->SetContextMenu(menu); + * + * // Show the tray icon + * tray_icon->SetVisible(true); + * ``` + */ +class TrayIcon : public EventEmitter, public NativeObjectProvider { + public: + /** + * @brief Default constructor for TrayIcon. + * + * Creates a new tray icon instance with platform-specific initialization. + * The icon will not be visible until SetVisible(true) is called. + * This constructor handles all platform-specific setup internally. + */ + TrayIcon(); + + /** + * @brief Constructor that wraps an existing platform-specific tray icon. + * + * This constructor is typically used internally by the TrayManager + * to wrap existing system tray icons. + * + * @param tray Pointer to the platform-specific tray icon object + */ + TrayIcon(void* tray); + + /** + * @brief Destructor for TrayIcon. + * + * Cleans up the tray icon and removes it from the system tray if visible. + * Also releases any associated platform-specific resources. + */ + virtual ~TrayIcon(); + + /** + * @brief Get the unique identifier for this tray icon. + * + * @return The unique identifier for this tray icon + */ + TrayIconId GetId(); + + /** + * @brief Set the icon image for the tray icon using an Image object. + * + * This is the preferred method for setting the tray icon image as it + * provides type safety and better control over image handling. + * + * @param image Shared pointer to an Image object, or nullptr to clear the icon + * + * @example + * ```cpp + * // Using file path + * auto icon = Image::FromFile("/path/to/icon.png"); + * trayIcon->SetIcon(icon); + * + * // Using base64 data + * auto icon = Image::FromBase64("data:image/png;base64,iVBORw0KGgo..."); + * trayIcon->SetIcon(icon); + * + * // Using raw RGBA data + * std::vector pixels = {...}; + * auto icon = Image::FromRawData(pixels.data(), 32, 32, ImagePixelFormat::RGBA32); + * trayIcon->SetIcon(icon); + * + * // Clear icon + * trayIcon->SetIcon(nullptr); + * ``` + */ + void SetIcon(std::shared_ptr image); + + /** + * @brief Get the current icon image of the tray icon. + * + * @return A shared pointer to the current Image object, or nullptr if no icon is set + */ + std::shared_ptr GetIcon() const; + + /** + * @brief Set the title text for the tray icon. + * + * On platforms that support it (primarily macOS), the title text + * is displayed next to the icon in the status bar. On other platforms, + * this may be used internally for identification purposes. + * + * @param title The title text to display, or std::nullopt to clear the title + * + * @note On Windows and most Linux desktop environments, tray icons + * do not display title text directly. + */ + void SetTitle(std::optional title); + + /** + * @brief Get the current title text of the tray icon. + * + * @return The current title text as an optional string, or std::nullopt if no title is set + */ + std::optional GetTitle(); + + /** + * @brief Set the tooltip text for the tray icon. + * + * The tooltip appears when the user hovers the mouse over the tray icon. + * This is supported on all platforms and is useful for providing + * additional context about the application's current state. + * + * @param tooltip The tooltip text to display on hover, or std::nullopt to clear the tooltip + * + * @example + * ```cpp + * tray_icon->SetTooltip("MyApp - Status: Connected"); + * tray_icon->SetTooltip(std::nullopt); // Clear tooltip + * ``` + */ + void SetTooltip(std::optional tooltip); + + /** + * @brief Get the current tooltip text of the tray icon. + * + * @return The current tooltip text as an optional string, or std::nullopt if no tooltip is set + */ + std::optional GetTooltip(); + + /** + * @brief Set the context menu for the tray icon. + * + * The context menu is displayed when the user right-clicks (or equivalent + * platform-specific action) on the tray icon. The menu provides the primary + * interface for user interaction with the application. + * + * @param menu The Menu object containing the context menu items + * + * @note The Menu object is copied internally, so the original menu + * object's lifetime doesn't need to extend beyond this call. + * + * @example + * ```cpp + * Menu context_menu; + * context_menu.AddItem(context_menu.CreateItem("Show Window")); + * context_menu.AddSeparator(); + * context_menu.AddItem(context_menu.CreateItem("Exit")); + * tray_icon->SetContextMenu(context_menu); + * ``` + */ + void SetContextMenu(std::shared_ptr menu); + + /** + * @brief Get the current context menu of the tray icon. + * + * @return A copy of the current context Menu object + */ + std::shared_ptr GetContextMenu(); + + /** + * @brief Set the context menu trigger behavior. + * + * Determines which mouse interactions will automatically display the + * context menu. By default, the trigger is set to None, requiring + * explicit control via OpenContextMenu() or by setting a trigger mode. + * + * @param trigger The desired trigger behavior + * + * @note When set to ContextMenuTrigger::None (default), the context menu + * will only appear when OpenContextMenu() is called explicitly, giving + * you full control over menu display through event listeners. + * + * @example + * ```cpp + * // Right click shows menu (common on Windows/Linux) + * tray_icon->SetContextMenuTrigger(ContextMenuTrigger::RightClicked); + * + * // Left click shows menu (common on some Linux environments and macOS) + * tray_icon->SetContextMenuTrigger(ContextMenuTrigger::Clicked); + * + * // Double click shows menu + * tray_icon->SetContextMenuTrigger(ContextMenuTrigger::DoubleClicked); + * + * // Manual control (default) - handle events yourself + * tray_icon->SetContextMenuTrigger(ContextMenuTrigger::None); + * tray_icon->AddListener([&](const auto& e) { + * // Custom logic before showing menu + * tray_icon->OpenContextMenu(); + * }); + * ``` + */ + void SetContextMenuTrigger(ContextMenuTrigger trigger); + + /** + * @brief Get the current context menu trigger behavior. + * + * @return The current ContextMenuTrigger setting + */ + ContextMenuTrigger GetContextMenuTrigger(); + + /** + * @brief Get the screen coordinates and dimensions of the tray icon. + * + * Returns the bounding rectangle of the tray icon in screen coordinates. + * This can be useful for positioning popup windows or dialogs relative + * to the tray icon. + * + * @return Rectangle containing the screen position and size of the tray icon + * + * @note The accuracy of this information varies by platform: + * - macOS: Precise bounds of the status item + * - Windows: Approximate location of the notification area + * - Linux: Depends on the desktop environment and system tray + * implementation + */ + Rectangle GetBounds(); + + /** + * @brief Set the visibility of the tray icon in the system tray. + * + * Controls whether the tray icon is visible in the system notification area. + * This method replaces the previous Show() and Hide() methods for a more + * unified interface. + * + * @param visible true to make the icon visible, false to hide it + * @return true if the visibility was successfully changed, false otherwise + * + * @note On some platforms, showing a tray icon may fail if the + * system tray is not available or if there are too many icons. + * + * @example + * ```cpp + * // Show the tray icon + * tray_icon->SetVisible(true); + * + * // Hide the tray icon + * tray_icon->SetVisible(false); + * ``` + */ + bool SetVisible(bool visible); + + /** + * @brief Check if the tray icon is currently visible. + * + * @return true if the icon is visible in the system tray, false otherwise + */ + bool IsVisible(); + + /** + * @brief Display the context menu at the tray icon's location. + * + * Opens the context menu at a default position near the tray icon. + * This is a convenience method that automatically determines an appropriate + * position based on the tray icon's current location. + * + * @return true if the menu was successfully opened, false otherwise + * + * @note The exact positioning behavior may vary by platform: + * - macOS: Menu appears below the status item + * - Windows: Menu appears near the notification area + * - Linux: Menu appears at cursor position or near tray area + * + * @example + * ```cpp + * // Open context menu at default location + * tray_icon->OpenContextMenu(); + * ``` + */ + bool OpenContextMenu(); + + /** + * @brief Close the currently displayed context menu. + * + * Closes the tray icon's context menu if it is currently visible. + * This allows for programmatic dismissal of the menu. + * + * @return true if the menu was successfully closed or wasn't visible, false + * on error + * + * @note This method is useful for keyboard shortcuts or programmatic control + * that needs to dismiss the context menu without user interaction. + * + * @example + * ```cpp + * // Close the context menu programmatically + * tray_icon->CloseContextMenu(); + * ``` + */ + bool CloseContextMenu(); + + protected: + /** + * @brief Called when the first listener is added. + * + * Subclasses can override this to start platform-specific event monitoring. + * This is called automatically by the EventEmitter when transitioning from + * 0 to 1+ listeners. + */ + void StartEventListening() override; + + /** + * @brief Called when the last listener is removed. + * + * Subclasses can override this to stop platform-specific event monitoring. + * This is called automatically by the EventEmitter when transitioning from + * 1+ to 0 listeners. + */ + void StopEventListening() override; + + /** + * @brief Internal method to get the platform-specific native tray icon object. + * + * This method must be implemented by platform-specific code to return + * the underlying native tray icon object. + * + * @return Pointer to the native menu item object + */ + void* GetNativeObjectInternal() const override; + + private: + /** + * @brief Private implementation class using the PIMPL idiom. + * + * This forward declaration hides the platform-specific implementation + * details from the public interface, allowing for better binary + * compatibility and cleaner separation of concerns. + */ + class Impl; + + /** + * @brief Pointer to the private implementation instance. + * + * This pointer manages the platform-specific implementation of + * the tray icon functionality. + */ + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/tray_manager.cpp b/packages/cnativeapi/cxx_impl/src/tray_manager.cpp new file mode 100644 index 0000000..0869d57 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/tray_manager.cpp @@ -0,0 +1,10 @@ +#include "tray_manager.h" + +namespace nativeapi { + +TrayManager& TrayManager::GetInstance() { + static TrayManager instance; + return instance; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/tray_manager.h b/packages/cnativeapi/cxx_impl/src/tray_manager.h new file mode 100644 index 0000000..5c6e64a --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/tray_manager.h @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include +#include + +#include "tray_icon.h" + +namespace nativeapi { + +/** + * @brief TrayManager is a singleton class that provides system tray functionality. + * + * This class provides centralized access to system tray capabilities and + * manages existing tray icons. It ensures that there's only one instance of + * the tray manager throughout the application lifetime and provides thread-safe + * operations for accessing tray icons. + * + * @note This class is implemented as a singleton to ensure consistent + * access to system tray resources across the entire application. + * @note TrayIcon instances should be created directly using std::make_shared() + * rather than through this manager. + */ +class TrayManager { + public: + /** + * @brief Get the singleton instance of TrayManager. + * + * This method provides access to the unique instance of TrayManager. + * The instance is created on first call and remains alive for the + * duration of the application. + * + * @return Reference to the singleton TrayManager instance + * @thread_safety This method is thread-safe + */ + static TrayManager& GetInstance(); + + /** + * @brief Destructor for TrayManager. + * + * Cleans up all managed tray icons and releases system resources. + */ + virtual ~TrayManager(); + + /** + * @brief Check if the system tray is supported on the current platform. + * + * Some platforms or desktop environments may not support system tray + * functionality. This method allows checking for availability before + * attempting to create tray icons. + * + * @return true if system tray is supported, false otherwise + */ + bool IsSupported(); + + /** + * @brief Get a tray icon by its unique ID. + * + * Retrieves a previously created tray icon using its assigned ID. + * + * @param id The unique identifier of the tray icon + * @return Shared pointer to the TrayIcon if found, nullptr otherwise + * @thread_safety This method is thread-safe + */ + std::shared_ptr Get(TrayIconId id); + + /** + * @brief Get all managed tray icons. + * + * Returns a vector containing all currently active tray icons + * managed by this TrayManager instance. + * + * @return Vector of shared pointers to all active TrayIcon instances + * @thread_safety This method is thread-safe + */ + std::vector> GetAll(); + + // Prevent copy construction and assignment to maintain singleton property + TrayManager(const TrayManager&) = delete; + TrayManager& operator=(const TrayManager&) = delete; + TrayManager(TrayManager&&) = delete; + TrayManager& operator=(TrayManager&&) = delete; + + private: + /** + * @brief Private constructor to enforce singleton pattern. + * + * Initializes the TrayManager instance and sets up initial state. + */ + TrayManager(); + + /** + * @brief Private implementation class using the PIMPL idiom. + */ + class Impl; + + /** + * @brief Pointer to the private implementation instance. + */ + std::unique_ptr pimpl_; + + /** + * @brief Container for storing active tray icon instances. + * + * Maps tray icon IDs to their corresponding TrayIcon instances + * for efficient lookup and management. + */ + std::unordered_map> trays_; + + /** + * @brief ID generator for creating unique tray icon identifiers. + * + * Maintains the next available ID to assign to newly created tray icons. + * This ensures each tray icon has a unique identifier. + */ + TrayIconId next_tray_id_; + + /** + * @brief Mutex for thread-safe operations. + * + * Protects access to internal data structures to ensure thread safety + * when multiple threads access the TrayManager simultaneously. + */ + mutable std::mutex mutex_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/url_opener.cpp b/packages/cnativeapi/cxx_impl/src/url_opener.cpp new file mode 100644 index 0000000..776cc3f --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/url_opener.cpp @@ -0,0 +1,103 @@ +#include "url_opener.h" + +#include +#include + +namespace nativeapi { +namespace { + +std::string Trim(const std::string& value) { + size_t start = 0; + while (start < value.size() && std::isspace(static_cast(value[start]))) { + ++start; + } + + size_t end = value.size(); + while (end > start && std::isspace(static_cast(value[end - 1]))) { + --end; + } + + return value.substr(start, end - start); +} + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return value; +} + +UrlOpenResult ValidateUrl(const std::string& raw_url) { + const std::string url = Trim(raw_url); + if (url.empty()) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvalidUrlEmpty; + result.error_message = "URL is empty."; + return result; + } + + const size_t scheme_separator = url.find(':'); + if (scheme_separator == std::string::npos || scheme_separator == 0) { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvalidUrlMissingScheme; + result.error_message = "URL must include an explicit scheme (http or https)."; + return result; + } + + const std::string scheme = ToLower(url.substr(0, scheme_separator)); + if (scheme != "http" && scheme != "https") { + UrlOpenResult result; + result.success = false; + result.error_code = UrlOpenErrorCode::kInvalidUrlUnsupportedScheme; + result.error_message = "Only http and https URLs are supported."; + return result; + } + + UrlOpenResult ok; + ok.success = true; + ok.error_code = UrlOpenErrorCode::kNone; + return ok; +} + +} // namespace + +// --------------------------------------------------------------------------- +// UrlOpener::Impl +// --------------------------------------------------------------------------- + +bool UrlOpener::Impl::CanOpen(const std::string& url) const { + (void)url; + return true; +} + +// --------------------------------------------------------------------------- +// UrlOpener +// --------------------------------------------------------------------------- + +UrlOpener& UrlOpener::GetInstance() { + static UrlOpener instance; + return instance; +} + +bool UrlOpener::CanOpen(const std::string& url) const { + if (!ValidateUrl(url).success) { + return false; + } + return pimpl_->CanOpen(url); +} + +UrlOpenResult UrlOpener::Open(const std::string& url) const { + UrlOpenResult result = ValidateUrl(url); + if (!result.success) { + return result; + } + return pimpl_->Open(url); +} + +bool UrlOpener::IsSupported() const { + return pimpl_->IsSupported(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/url_opener.h b/packages/cnativeapi/cxx_impl/src/url_opener.h new file mode 100644 index 0000000..2de15fc --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/url_opener.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace nativeapi { + +enum class UrlOpenErrorCode { + kNone = 0, + kInvalidUrlEmpty, + kInvalidUrlMissingScheme, + kInvalidUrlUnsupportedScheme, + kUnsupportedPlatform, + kInvocationFailed, +}; + +struct UrlOpenResult { + bool success = false; + UrlOpenErrorCode error_code = UrlOpenErrorCode::kNone; + std::string error_message; +}; + +class UrlOpener { + public: + static UrlOpener& GetInstance(); + + bool IsSupported() const; + bool CanOpen(const std::string& url) const; + UrlOpenResult Open(const std::string& url) const; + + UrlOpener(const UrlOpener&) = delete; + UrlOpener& operator=(const UrlOpener&) = delete; + UrlOpener(UrlOpener&&) = delete; + UrlOpener& operator=(UrlOpener&&) = delete; + + class Impl { + public: + virtual ~Impl() = default; + + virtual bool IsSupported() const = 0; + virtual bool CanOpen(const std::string& url) const; + virtual UrlOpenResult Open(const std::string& url) const = 0; + }; + + private: + UrlOpener(); + ~UrlOpener(); + + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/window.h b/packages/cnativeapi/cxx_impl/src/window.h new file mode 100644 index 0000000..d884df9 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/window.h @@ -0,0 +1,941 @@ +#pragma once +#include +#include +#include "foundation/color.h" +#include "foundation/event.h" +#include "foundation/geometry.h" +#include "foundation/id_allocator.h" +#include "foundation/native_object_provider.h" + +namespace nativeapi { + +/** + * @typedef WindowId + * @brief Unique identifier for a window instance. + * + * This type is used to uniquely identify window instances across the system. + * Each window gets assigned a unique ID when created. + */ +typedef IdAllocator::IdType WindowId; + +/** + * @brief Title bar style options for windows. + * + * Defines how a window's title bar should be displayed. This affects the + * appearance and visibility of the standard window title bar including the + * title text and window control buttons (minimize, maximize, close). + * + * @note Platform behavior may vary: + * - Windows: Hidden style removes the title bar but may retain window borders + * - macOS: Hidden style creates a borderless window with transparent title bar + * - Linux: Hidden style removes window decorations entirely + */ +enum class TitleBarStyle { + /** + * Standard title bar with default platform appearance. + * Shows title text and standard window control buttons. + */ + Normal, + + /** + * Hidden title bar with no visible decorations. + * The window appears without a title bar, useful for custom chrome. + */ + Hidden +}; + +/** + * @brief Visual effect styles for window background. + * + * Defines blur or material effects applied to the window background. + * These effects typically provide a translucent or "frosted glass" appearance. + */ +enum class VisualEffect { + /** No visual effect. Standard solid background. */ + None, + + /** + * Standard background blur. + * - Windows: Standard blur (Blur behind) + * - macOS: Default vibrancy effect + */ + Blur, + + /** + * Enhanced translucent blur effect. + * - Windows: Acrylic effect + * - macOS: Thick vibrancy + */ + Acrylic, + + /** + * Material effect that samples the desktop wallpaper. + * - Windows: Mica effect (Windows 11+) + * - macOS: WindowBackground vibrancy + */ + Mica +}; + +/** + * @class Window + * @brief Cross-platform window abstraction class. + * + * This class provides a unified interface for creating and managing windows + * across different operating systems. It encapsulates all window-related + * functionality including size, position, visibility, focus, and appearance. + * + * The Window class uses the PIMPL idiom to hide platform-specific implementation + * details and provide a clean, consistent API across all supported platforms. + * + * @note This class is not thread-safe. All window operations should be performed + * on the main UI thread. + */ +class Window : public NativeObjectProvider, public std::enable_shared_from_this { + public: + /** + * @brief Default constructor creates a new window with default settings. + * + * Creates a new window with platform-default size, position, and properties. + * The window is initially hidden and must be explicitly shown. + * The window is automatically registered in the WindowRegistry. + */ + Window(); + + /** + * @brief Constructor that wraps an existing native window object. + * + * @param window Pointer to an existing platform-specific window object + * @note The Window instance takes ownership of the native window object + */ + Window(void* native_window); + + /** + * @brief Virtual destructor ensures proper cleanup of resources. + * + * Destroys the window and releases all associated resources including + * the native window object. + */ + virtual ~Window(); + + /** + * @brief Gets the unique identifier for this window. + * + * @return WindowId The unique identifier assigned to this window + */ + WindowId GetId() const; + + // === Focus Management === + + /** + * @brief Brings the window to the front and gives it keyboard focus. + * + * Makes this window the active window and brings it to the foreground. + * The window will receive keyboard input after this call. + */ + void Focus(); + + /** + * @brief Removes keyboard focus from the window. + * + * The window will no longer receive keyboard input, but remains visible. + * Focus may be transferred to another window or removed entirely. + */ + void Blur(); + + /** + * @brief Checks if the window currently has keyboard focus. + * + * @return true if the window has focus, false otherwise + */ + bool IsFocused() const; + + // === Visibility Management === + + /** + * @brief Shows the window and brings it to the front. + * + * Makes the window visible and typically gives it focus. If the window + * was minimized, it will be restored to its previous state. + */ + void Show(); + + /** + * @brief Shows the window without giving it focus. + * + * Makes the window visible but does not change the currently focused window. + * Useful for showing auxiliary windows or notifications. + */ + void ShowInactive(); + + /** + * @brief Hides the window from view. + * + * Makes the window invisible but does not destroy it. The window can + * be shown again later with Show() or ShowInactive(). + */ + void Hide(); + + /** + * @brief Checks if the window is currently visible. + * + * @return true if the window is visible, false if hidden or minimized + */ + bool IsVisible() const; + // === Window State Management === + + /** + * @brief Maximizes the window to fill the available screen space. + * + * Expands the window to occupy the maximum available area on the screen, + * typically excluding taskbars and docks. + */ + void Maximize(); + + /** + * @brief Restores the window from maximized state to its previous size. + * + * Returns the window to the size and position it had before being maximized. + */ + void Unmaximize(); + + /** + * @brief Checks if the window is currently maximized. + * + * @return true if the window is maximized, false otherwise + */ + bool IsMaximized() const; + + /** + * @brief Minimizes the window, hiding it from the desktop. + * + * Reduces the window to an icon in the taskbar or dock. The window + * remains open but is not visible on the desktop. + */ + void Minimize(); + + /** + * @brief Restores the window from minimized or maximized state. + * + * Returns the window to its normal state and size. If the window was + * minimized, it becomes visible again. If maximized, it returns to + * its previous non-maximized size. + */ + void Restore(); + + /** + * @brief Checks if the window is currently minimized. + * + * @return true if the window is minimized, false otherwise + */ + bool IsMinimized() const; + + /** + * @brief Sets the window's fullscreen state. + * + * @param is_full_screen true to enter fullscreen mode, false to exit + * + * In fullscreen mode, the window occupies the entire screen with no + * window decorations (title bar, borders) visible. + */ + void SetFullScreen(bool is_full_screen); + + /** + * @brief Checks if the window is currently in fullscreen mode. + * + * @return true if the window is fullscreen, false otherwise + */ + bool IsFullScreen() const; + // === Size and Bounds Management === + + // void SetBackgroundColor(Color color); + // Color GetBackgroundColor() const; + + /** + * @brief Sets the window's position and size simultaneously. + * + * @param bounds Rectangle containing the desired position and size + * + * This method sets both the window's position and size in a single operation, + * which can be more efficient than separate calls to SetPosition() and SetSize(). + */ + void SetBounds(Rectangle bounds); + + /** + * @brief Gets the window's current position and size. + * + * @return Rectangle containing the current position and size of the window + * + * The returned rectangle includes the window frame and decorations. + */ + Rectangle GetBounds() const; + + /** + * @brief Sets the position and size of the window's content area. + * + * @param bounds Rectangle containing the desired position and size of the content area + * + * This method sets both the content area's position and size in a single operation, + * which can be more efficient than separate calls to SetPosition() and SetContentSize(). + * The content area excludes window decorations like title bar and borders. + */ + void SetContentBounds(Rectangle bounds); + + /** + * @brief Gets the position and size of the window's content area. + * + * @return Rectangle containing the current position and size of the content area + * + * The returned rectangle excludes window decorations and represents the drawable + * content area of the window. + */ + Rectangle GetContentBounds() const; + + /** + * @brief Sets the window's size with optional animation. + * + * @param size The new size for the window + * @param animate Whether to animate the size change + * + * Changes the window's outer size including frame and decorations. + * If animate is true, the resize will be smoothly animated on supported platforms. + */ + void SetSize(Size size, bool animate); + + /** + * @brief Gets the window's current outer size. + * + * @return Size The current size of the window including frame and decorations + */ + Size GetSize() const; + + /** + * @brief Sets the size of the window's content area. + * + * @param size The desired size of the content area + * + * This sets the size of the drawable content area, excluding window + * decorations like title bar and borders. The actual window size will + * be larger to accommodate the frame. + */ + void SetContentSize(Size size); + + /** + * @brief Gets the size of the window's content area. + * + * @return Size The current size of the content area excluding decorations + */ + Size GetContentSize() const; + + /** + * @brief Sets the minimum size the window can be resized to. + * + * @param size The minimum allowed size + * + * Prevents the user from resizing the window smaller than the specified size. + * This applies to the outer window size including decorations. + */ + void SetMinimumSize(Size size); + + /** + * @brief Gets the current minimum size constraint. + * + * @return Size The minimum size the window can be resized to + */ + Size GetMinimumSize() const; + + /** + * @brief Sets the maximum size the window can be resized to. + * + * @param size The maximum allowed size + * + * Prevents the user from resizing the window larger than the specified size. + * This applies to the outer window size including decorations. + */ + void SetMaximumSize(Size size); + + /** + * @brief Gets the current maximum size constraint. + * + * @return Size The maximum size the window can be resized to + */ + Size GetMaximumSize() const; + // === Window Behavior Properties === + + /** + * @brief Sets whether the window can be resized by the user. + * + * @param is_resizable true to allow resizing, false to disable + * + * When disabled, the user cannot resize the window by dragging its edges + * or corners. Programmatic resizing via SetSize() is still possible. + */ + void SetResizable(bool is_resizable); + + /** + * @brief Checks if the window can be resized by the user. + * + * @return true if user can resize the window, false otherwise + */ + bool IsResizable() const; + + /** + * @brief Sets whether the window can be moved by the user. + * + * @param is_movable true to allow moving, false to disable + * + * When disabled, the user cannot move the window by dragging its title bar. + * Programmatic positioning via SetPosition() is still possible. + */ + void SetMovable(bool is_movable); + + /** + * @brief Checks if the window can be moved by the user. + * + * @return true if user can move the window, false otherwise + */ + bool IsMovable() const; + + /** + * @brief Sets whether the window can be minimized by the user. + * + * @param is_minimizable true to allow minimizing, false to disable + * + * Controls the availability of minimize functionality in the window's + * title bar and system menu. Programmatic minimizing is still possible. + */ + void SetMinimizable(bool is_minimizable); + + /** + * @brief Checks if the window can be minimized by the user. + * + * @return true if user can minimize the window, false otherwise + */ + bool IsMinimizable() const; + + /** + * @brief Sets whether the window can be maximized by the user. + * + * @param is_maximizable true to allow maximizing, false to disable + * + * Controls the availability of maximize functionality in the window's + * title bar and system menu. Programmatic maximizing is still possible. + */ + void SetMaximizable(bool is_maximizable); + + /** + * @brief Checks if the window can be maximized by the user. + * + * @return true if user can maximize the window, false otherwise + */ + bool IsMaximizable() const; + + /** + * @brief Sets whether the window can enter fullscreen mode. + * + * @param is_full_screenable true to allow fullscreen, false to disable + * + * Controls whether the window supports fullscreen mode. On some platforms, + * this affects the availability of fullscreen controls in the UI. + */ + void SetFullScreenable(bool is_full_screenable); + + /** + * @brief Checks if the window supports fullscreen mode. + * + * @return true if fullscreen is supported, false otherwise + */ + bool IsFullScreenable() const; + + /** + * @brief Sets whether the window can be closed by the user. + * + * @param is_closable true to allow closing, false to disable + * + * When disabled, the close button in the title bar is hidden or disabled. + * The window can still be closed programmatically. + */ + void SetClosable(bool is_closable); + + /** + * @brief Checks if the window can be closed by the user. + * + * @return true if user can close the window, false otherwise + */ + bool IsClosable() const; + + /** + * @brief Sets the visibility of window control buttons. + * + * @param is_visible true to show window control buttons, false to hide them + * + * Controls the visibility of window control buttons (minimize, maximize, close) + * in the title bar. When hidden, the buttons are not visible but the window + * can still be controlled programmatically. + * + * @note Platform availability: + * - macOS: ✅ Fully supported - Hides/shows the traffic light buttons (red, yellow, green) + * - Windows: ❌ Not implemented - Returns default value (visible) + * - Linux: ❌ Not implemented - Returns default value (visible) + * - Android: ❌ Not applicable - Mobile apps don't have window control buttons + * - iOS: ❌ Not applicable - Mobile apps don't have window control buttons + * - OpenHarmony: ❌ Not applicable - Mobile apps don't have window control buttons + */ + void SetWindowControlButtonsVisible(bool is_visible); + + /** + * @brief Checks if the window control buttons are visible. + * + * @return true if window control buttons are visible, false if hidden + * + * @note Platform availability: + * - macOS: ✅ Fully supported - Returns actual visibility state + * - Windows: ❌ Not implemented - Always returns true + * - Linux: ❌ Not implemented - Always returns true + * - Android: ❌ Not applicable - Always returns false + * - iOS: ❌ Not applicable - Always returns false + * - OpenHarmony: ❌ Not applicable - Always returns false + */ + bool IsWindowControlButtonsVisible() const; + + /** + * @brief Sets whether the window stays on top of other windows. + * + * @param is_always_on_top true to keep on top, false for normal behavior + * + * When enabled, the window will remain visible above other windows + * even when it doesn't have focus. + */ + void SetAlwaysOnTop(bool is_always_on_top); + + /** + * @brief Checks if the window is set to always stay on top. + * + * @return true if window stays on top, false otherwise + */ + bool IsAlwaysOnTop() const; + + // === Position and Title === + + /** + * @brief Sets the window's position on the screen. + * + * @param point The new position for the window's top-left corner + * + * Coordinates are relative to the screen's origin (typically top-left). + */ + void SetPosition(Point point); + + /** + * @brief Gets the window's current position on the screen. + * + * @return Point The position of the window's top-left corner + */ + Point GetPosition() const; + + /** + * @brief Centers the window on the screen. + * + * Moves the window to the center of the primary display. The window + * will be positioned so that its center point aligns with the center + * of the screen. + */ + void Center(); + + /** + * @brief Sets the text displayed in the window's title bar. + * + * @param title The new title text for the window + */ + void SetTitle(std::string title); + + /** + * @brief Gets the current title text of the window. + * + * @return std::string The current title displayed in the title bar + */ + std::string GetTitle() const; + + /** + * @brief Sets the style of the window's title bar. + * + * @param style The desired title bar style + * + * Controls the appearance and visibility of the window's title bar. + * Use TitleBarStyle::Normal for standard appearance or TitleBarStyle::Hidden + * to create a frameless window without title bar decorations. + * + * @note When using Hidden style, you may want to implement custom window + * controls and dragging behavior using StartDragging(). + */ + void SetTitleBarStyle(TitleBarStyle style); + + /** + * @brief Gets the current title bar style of the window. + * + * @return TitleBarStyle The current title bar style + */ + TitleBarStyle GetTitleBarStyle() const; + // === Appearance and Advanced Behavior === + + /** + * @brief Sets whether the window displays a shadow. + * + * @param has_shadow true to show shadow, false to hide it + * + * Controls the drop shadow effect around the window. On some platforms, + * this may affect window compositing and visual effects. + */ + void SetHasShadow(bool has_shadow); + + /** + * @brief Checks if the window currently displays a shadow. + * + * @return true if shadow is enabled, false otherwise + */ + bool HasShadow() const; + + /** + * @brief Sets the window's opacity (transparency level). + * + * @param opacity Opacity value between 0.0 (fully transparent) and 1.0 (fully opaque) + * + * Controls the transparency of the entire window including its content. + * Values outside the 0.0-1.0 range will be clamped to valid values. + */ + void SetOpacity(float opacity); + + /** + * @brief Gets the window's current opacity level. + * + * @return float Current opacity value between 0.0 and 1.0 + */ + float GetOpacity() const; + + /** + * @brief Sets the visual effect (blur/vibrancy) for the window background. + * + * Allows creating translucent windows with various platform-specific effects. + * + * @param effect The visual effect to apply + */ + void SetVisualEffect(VisualEffect effect); + + /** + * @brief Gets the current visual effect applied to the window. + * + * @return VisualEffect The current visual effect + */ + VisualEffect GetVisualEffect() const; + + /** + * @brief Sets the background color of the window. + * + * Sets a solid color for the window background. This color will be visible + * if the window content does not fully cover the window area, or if visual + * effects are enabled. + * + * @param color The background color to apply + * + * @note Platform behavior may vary: + * - Windows: Sets the window background brush color + * - macOS: Sets the window backgroundColor property + * - Linux: Sets the window background color via GTK/X11 + */ + void SetBackgroundColor(const Color& color); + + /** + * @brief Gets the current background color of the window. + * + * @return Color The current background color + */ + Color GetBackgroundColor() const; + + /** + * @brief Sets whether the window appears on all virtual desktops/workspaces. + * + * @param is_visible_on_all_workspaces true to appear on all workspaces, false for current only + * + * When enabled, the window will be visible regardless of which virtual + * desktop or workspace the user switches to. Platform support may vary. + */ + void SetVisibleOnAllWorkspaces(bool is_visible_on_all_workspaces); + + /** + * @brief Checks if the window appears on all workspaces. + * + * @return true if visible on all workspaces, false if only on current workspace + */ + bool IsVisibleOnAllWorkspaces() const; + + /** + * @brief Sets whether the window ignores mouse input events. + * + * @param is_ignore_mouse_events true to ignore mouse events, false to receive them + * + * When enabled, mouse events (clicks, hovers, etc.) pass through the window + * to whatever is behind it. Useful for overlay or heads-up display windows. + */ + void SetIgnoreMouseEvents(bool is_ignore_mouse_events); + + /** + * @brief Checks if the window ignores mouse events. + * + * @return true if mouse events are ignored, false if they are received + */ + bool IsIgnoreMouseEvents() const; + + /** + * @brief Sets whether the window can receive keyboard focus. + * + * @param is_focusable true to allow focus, false to prevent it + * + * When disabled, the window cannot receive keyboard focus and will not + * respond to keyboard input. Useful for utility or informational windows. + */ + void SetFocusable(bool is_focusable); + + /** + * @brief Checks if the window can receive keyboard focus. + * + * @return true if the window can be focused, false otherwise + */ + bool IsFocusable() const; + + // === User Interaction === + + /** + * @brief Initiates a user drag operation for moving the window. + * + * Allows the user to drag the window by clicking and dragging anywhere + * within the window's content area, not just the title bar. This is + * commonly used for frameless windows or custom title bars. + */ + void StartDragging(); + + /** + * @brief Initiates a user resize operation for the window. + * + * Allows the user to resize the window by dragging from the current + * mouse position. The resize behavior depends on the current cursor + * position relative to the window edges. + */ + void StartResizing(); + + protected: + /** + * @brief Internal method to get the platform-specific native window object. + * + * This method must be implemented by platform-specific code to return + * the underlying native window object. + * + * @return Pointer to the native window object + */ + void* GetNativeObjectInternal() const override; + + private: + /** + * @brief Forward declaration of platform-specific implementation class. + * + * This class uses the PIMPL (Pointer to Implementation) idiom to hide + * platform-specific details and reduce compilation dependencies. + */ + class Impl; + + /** @brief Pointer to the platform-specific implementation */ + std::unique_ptr pimpl_; +}; + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +/** + * Base class for all window-related events + * + * This class provides common functionality for window events, + * including access to the window ID that triggered the event. + */ +class WindowEvent : public Event { + public: + /** + * Constructor for WindowEvent + * @param window_id The window ID associated with this event + */ + explicit WindowEvent(WindowId window_id) : window_id_(window_id) {} + + /** + * Virtual destructor + */ + virtual ~WindowEvent() = default; + + /** + * Get the window ID associated with this event + * @return The window ID + */ + WindowId GetWindowId() const { return window_id_; } + + /** + * Get a string representation of the event type (for debugging) + * Default implementation returns "WindowEvent" + */ + std::string GetTypeName() const override { return "WindowEvent"; } + + private: + WindowId window_id_; +}; + +/** + * Event class for window focus gained + * + * This event is emitted when a window gains focus and becomes the active window. + */ +class WindowFocusedEvent : public WindowEvent { + public: + explicit WindowFocusedEvent(WindowId window_id) : WindowEvent(window_id) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowFocusedEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for window focus lost + * + * This event is emitted when a window loses focus and is no longer the active window. + */ +class WindowBlurredEvent : public WindowEvent { + public: + explicit WindowBlurredEvent(WindowId window_id) : WindowEvent(window_id) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowBlurredEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for window minimized + * + * This event is emitted when a window is minimized to the taskbar or dock. + */ +class WindowMinimizedEvent : public WindowEvent { + public: + explicit WindowMinimizedEvent(WindowId window_id) : WindowEvent(window_id) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowMinimizedEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for window maximized + * + * This event is emitted when a window is maximized to fill the entire screen. + */ +class WindowMaximizedEvent : public WindowEvent { + public: + explicit WindowMaximizedEvent(WindowId window_id) : WindowEvent(window_id) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowMaximizedEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for window restored + * + * This event is emitted when a window is restored from minimized or maximized state + * to its normal windowed state. + */ +class WindowRestoredEvent : public WindowEvent { + public: + explicit WindowRestoredEvent(WindowId window_id) : WindowEvent(window_id) {} + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowRestoredEvent"; } + + /** + * Get the static type index for this event type + */ +}; + +/** + * Event class for window moved + * + * This event is emitted when a window is moved to a new position on the screen. + */ +class WindowMovedEvent : public WindowEvent { + public: + WindowMovedEvent(WindowId window_id, Point new_position) + : WindowEvent(window_id), new_position_(new_position) {} + + /** + * Get the new position of the window + * @return The new position as a Point + */ + Point GetNewPosition() const { return new_position_; } + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowMovedEvent"; } + + /** + * Get the static type index for this event type + */ + + private: + Point new_position_; +}; + +/** + * Event class for window resized + * + * This event is emitted when a window is resized to a new size. + */ +class WindowResizedEvent : public WindowEvent { + public: + WindowResizedEvent(WindowId window_id, Size new_size) + : WindowEvent(window_id), new_size_(new_size) {} + + /** + * Get the new size of the window + * @return The new size as a Size object + */ + Size GetNewSize() const { return new_size_; } + + /** + * Get a string representation of the event type + */ + std::string GetTypeName() const override { return "WindowResizedEvent"; } + + /** + * Get the static type index for this event type + */ + + private: + Size new_size_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/window_manager.cpp b/packages/cnativeapi/cxx_impl/src/window_manager.cpp new file mode 100644 index 0000000..31b9476 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/window_manager.cpp @@ -0,0 +1,10 @@ +#include "window_manager.h" + +namespace nativeapi { + +WindowManager& WindowManager::GetInstance() { + static WindowManager instance; + return instance; +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/window_manager.h b/packages/cnativeapi/cxx_impl/src/window_manager.h new file mode 100644 index 0000000..97a6e33 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/window_manager.h @@ -0,0 +1,210 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "foundation/event.h" +#include "foundation/event_emitter.h" +#include "foundation/geometry.h" +#include "window.h" + +namespace nativeapi { + +/** + * @brief WindowManager is a singleton that manages all windows in the application + * + * The WindowManager provides a centralized interface for creating, managing, and + * monitoring windows across the entire application. It follows the singleton pattern + * to ensure there's only one instance managing all windows, and provides event + * notifications for various window state changes. + * + * Key features: + * - Singleton pattern ensures single point of window management + * - Event-driven architecture for window state notifications + * - Cross-platform window creation and management + * - Thread-safe access to the singleton instance + * - Automatic cleanup of resources on destruction + * + * @note This class is thread-safe for singleton access, but individual operations + * may require additional synchronization depending on the platform implementation. + */ +class WindowManager : public EventEmitter { + public: + /** + * @brief Get the singleton instance of WindowManager + * + * This method provides access to the unique instance of WindowManager using + * the Meyer's singleton pattern. The instance is created on first call and + * remains alive for the duration of the application. This method is thread-safe + * and guarantees that only one instance will be created even in multi-threaded + * environments. + * + * @return Reference to the singleton WindowManager instance + * @thread_safety This method is thread-safe + * + * @code + * // Usage example: + * auto& manager = WindowManager::GetInstance(); + * auto window = manager.Create(options); + * @endcode + */ + static WindowManager& GetInstance(); + + /** + * @brief Destructor + * + * Cleans up all resources, closes remaining windows, and stops event monitoring. + * This is automatically called when the application terminates. + */ + virtual ~WindowManager(); + + /** + * @brief Get a window by its unique ID + * + * Retrieves a window instance from the internal registry using its ID. + * This method is useful for accessing windows when you have their ID + * from events or other sources. + * + * @param id The unique identifier of the window to retrieve + * @return Shared pointer to the Window instance, or nullptr if window not found + * + * @code + * WindowId window_id = some_event.GetWindowId(); + * auto window = WindowManager::GetInstance().Get(window_id); + * if (window) { + * window->Show(); + * } + * @endcode + */ + std::shared_ptr Get(WindowId id); + + /** + * @brief Get all managed windows + * + * Returns a vector containing all currently managed window instances. + * The returned vector is a snapshot of the current state and modifications + * to it won't affect the internal window registry. + * + * @return Vector of shared pointers to all Window instances + * + * @code + * auto all_windows = WindowManager::GetInstance().GetAll(); + * for (auto& window : all_windows) { + * window->Hide(); // Hide all windows + * } + * @endcode + */ + std::vector> GetAll(); + + /** + * @brief Get the currently active/focused window + * + * Returns the window that currently has keyboard focus and is active. + * This is typically the window that the user is currently interacting with. + * + * @return Shared pointer to the current Window instance, or nullptr if no window is active + * + * @code + * auto current = WindowManager::GetInstance().GetCurrent(); + * if (current) { + * std::cout << "Active window: " << current->GetTitle() << std::endl; + * } + * @endcode + */ + std::shared_ptr GetCurrent(); + + /** + * Hooks invoked before native window show/hide operations (e.g., via swizzling). + * These are declarations only; platform implementations can register and invoke them. + */ + using WindowWillShowHook = std::function; + using WindowWillHideHook = std::function; + using WindowWillCloseHook = std::function; + + // Set or clear single hooks (pass std::nullopt to clear) + void SetWillShowHook(std::optional hook); + void SetWillHideHook(std::optional hook); + void SetWillCloseHook(std::optional hook); + + // Check if hooks are set + bool HasWillShowHook() const; + bool HasWillHideHook() const; + bool HasWillCloseHook() const; + + // Called by platform layer BEFORE the actual show/hide/close happens + void HandleWillShow(WindowId id); + void HandleWillHide(WindowId id); + void HandleWillClose(WindowId id); + + /** + * Call the platform's original show/hide/close implementations for a window, + * bypassing swizzled paths. Returns true if successfully invoked. + * On unsupported platforms, these return false. + */ + bool CallOriginalShow(WindowId id); + bool CallOriginalHide(WindowId id); + bool CallOriginalClose(WindowId id); + + protected: + /** + * @brief Called when the first listener is added. + * + * Subclasses can override this to start platform-specific event monitoring. + * This is called automatically by the EventEmitter when transitioning from + * 0 to 1+ listeners. + */ + void StartEventListening() override; + + /** + * @brief Called when the last listener is removed. + * + * Subclasses can override this to stop platform-specific event monitoring. + * This is called automatically by the EventEmitter when transitioning from + * 1+ to 0 listeners. + */ + void StopEventListening() override; + + private: + /** + * @brief Private constructor to enforce singleton pattern + * + * Initializes the WindowManager instance, sets up platform-specific + * event monitoring, and prepares the internal data structures. + * This constructor is private to prevent direct instantiation. + */ + WindowManager(); + + // Prevent copy construction and assignment to maintain singleton property + WindowManager(const WindowManager&) = delete; + WindowManager& operator=(const WindowManager&) = delete; + WindowManager(WindowManager&&) = delete; + WindowManager& operator=(WindowManager&&) = delete; + + /** + * @brief Platform-specific implementation details + * + * Uses the PIMPL (Pointer to Implementation) idiom to hide platform-specific + * details and reduce compilation dependencies. + */ + class Impl; + std::unique_ptr pimpl_; + + // Window instances are tracked by WindowRegistry (see window_registry.h) + + /** + * @brief Internal method to dispatch window events + * + * Processes window events received from the platform and dispatches them + * to registered event listeners. This method is called by the platform-specific + * event monitoring system. + * + * @param event The window event to dispatch + */ + void DispatchWindowEvent(const WindowEvent& event); +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/window_registry.cpp b/packages/cnativeapi/cxx_impl/src/window_registry.cpp new file mode 100644 index 0000000..921bb12 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/window_registry.cpp @@ -0,0 +1,44 @@ +#include "window_registry.h" +#include "foundation/object_registry.h" +#include "window.h" + +namespace nativeapi { + +class WindowRegistry::Impl { + public: + ObjectRegistry registry_; +}; + +WindowRegistry& WindowRegistry::GetInstance() { + static WindowRegistry instance; + return instance; +} + +WindowRegistry::WindowRegistry() : pimpl_(std::make_unique()) {} +WindowRegistry::~WindowRegistry() = default; + +void WindowRegistry::Add(WindowId id, const std::shared_ptr& window) { + pimpl_->registry_.Add(id, window); +} + +std::shared_ptr WindowRegistry::Get(WindowId id) const { + return pimpl_->registry_.Get(id); +} + +std::vector> WindowRegistry::GetAll() const { + return pimpl_->registry_.GetAll(); +} + +bool WindowRegistry::Contains(WindowId id) const { + return pimpl_->registry_.Contains(id); +} + +bool WindowRegistry::Remove(WindowId id) { + return pimpl_->registry_.Remove(id); +} + +void WindowRegistry::Clear() { + pimpl_->registry_.Clear(); +} + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/src/window_registry.h b/packages/cnativeapi/cxx_impl/src/window_registry.h new file mode 100644 index 0000000..69cddc1 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/window_registry.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include "foundation/id_allocator.h" + +namespace nativeapi { + +class Window; + +using WindowId = IdAllocator::IdType; + +class WindowRegistry { + public: + static WindowRegistry& GetInstance(); + + void Add(WindowId id, const std::shared_ptr& window); + std::shared_ptr Get(WindowId id) const; + std::vector> GetAll() const; + bool Contains(WindowId id) const; + bool Remove(WindowId id); + void Clear(); + + private: + WindowRegistry(); + ~WindowRegistry(); + + class Impl; + std::unique_ptr pimpl_; +}; + +} // namespace nativeapi diff --git a/packages/cnativeapi/cxx_impl/tests/CMakeLists.txt b/packages/cnativeapi/cxx_impl/tests/CMakeLists.txt new file mode 100644 index 0000000..e8e3ca6 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(url_opener_test url_opener_test.cpp) +target_link_libraries(url_opener_test PRIVATE nativeapi) +add_test(NAME url_opener_test COMMAND url_opener_test) + +# Links the full library because EventEmitter now routes async delivery through +# the platform dispatcher. The test installs its own dispatcher (see +# FakeMainThread) so no run loop is required at runtime. +find_package(Threads REQUIRED) +add_executable(event_emitter_test event_emitter_test.cpp) +target_link_libraries(event_emitter_test PRIVATE nativeapi Threads::Threads) +add_test(NAME event_emitter_test COMMAND event_emitter_test) +set_tests_properties(event_emitter_test PROPERTIES TIMEOUT 120) + +add_executable(id_allocator_test id_allocator_test.cpp) +target_link_libraries(id_allocator_test PRIVATE nativeapi Threads::Threads) +add_test(NAME id_allocator_test COMMAND id_allocator_test) + +add_executable(handle_table_test handle_table_test.cpp) +target_link_libraries(handle_table_test PRIVATE nativeapi Threads::Threads) +add_test(NAME handle_table_test COMMAND handle_table_test) +set_tests_properties(handle_table_test PROPERTIES TIMEOUT 120) + +add_executable(shortcut_accelerator_test shortcut_accelerator_test.cpp) +target_link_libraries(shortcut_accelerator_test PRIVATE nativeapi) +add_test(NAME shortcut_accelerator_test COMMAND shortcut_accelerator_test) + +find_package(Threads REQUIRED) +add_executable(window_manager_hook_test window_manager_hook_test.cpp) +target_link_libraries(window_manager_hook_test PRIVATE nativeapi Threads::Threads) +add_test(NAME window_manager_hook_test COMMAND window_manager_hook_test) +set_tests_properties(window_manager_hook_test PROPERTIES TIMEOUT 120) diff --git a/packages/cnativeapi/cxx_impl/tests/event_emitter_test.cpp b/packages/cnativeapi/cxx_impl/tests/event_emitter_test.cpp new file mode 100644 index 0000000..0c7b897 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/event_emitter_test.cpp @@ -0,0 +1,526 @@ +// Regression tests for EventEmitter locking and dispatch behaviour. +// +// Every case here maps to a defect described in DESIGN_REVIEW.md: +// P0-1 EmitAsync self-deadlock (recursive lock on queue_mutex_) +// P0-2 Emit invoking listener callbacks while holding listeners_mutex_ +// +// A deadlock must surface as a test FAILURE, not as a hung CI job, so the whole +// run is guarded by a watchdog thread that aborts the process on timeout. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/foundation/dispatcher.h" +#include "../src/foundation/event_emitter.h" + +namespace { + +using namespace nativeapi; + +// --------------------------------------------------------------------------- +// Fake main thread +// --------------------------------------------------------------------------- + +// A test binary has no run loop, so the platform dispatcher would queue work +// that never runs. Route dispatch into a queue this file drains explicitly, and +// let the test decide which thread counts as "main". +class FakeMainThread { + public: + FakeMainThread() : main_thread_id_(std::this_thread::get_id()) { + SetMainThreadDispatcher( + [this](std::function fn) { + std::lock_guard lock(mutex_); + queue_.push_back(std::move(fn)); + return true; + }, + [this] { return std::this_thread::get_id() == main_thread_id_; }); + } + + ~FakeMainThread() { SetMainThreadDispatcher(nullptr, nullptr); } + + /** Runs everything queued so far. Returns how many items ran. */ + size_t Drain() { + std::vector> batch; + { + std::lock_guard lock(mutex_); + batch.swap(queue_); + } + for (auto& fn : batch) { + fn(); + } + return batch.size(); + } + + size_t PendingCount() { + std::lock_guard lock(mutex_); + return queue_.size(); + } + + private: + std::mutex mutex_; + std::vector> queue_; + std::thread::id main_thread_id_; +}; + +FakeMainThread* g_main_thread = nullptr; + +// --------------------------------------------------------------------------- +// Test scaffolding +// --------------------------------------------------------------------------- + +int g_failures = 0; + +void Check(bool condition, const std::string& what) { + if (!condition) { + std::cerr << "FAIL: " << what << std::endl; + ++g_failures; + } else { + std::cout << " ok: " << what << std::endl; + } +} + +// Aborts the process if the suite does not finish in time. Without this, a +// regression of the locking bugs would hang the test binary forever. +class Watchdog { + public: + explicit Watchdog(std::chrono::seconds timeout) : done_(false) { + thread_ = std::thread([this, timeout] { + std::unique_lock lock(mutex_); + if (!cv_.wait_for(lock, timeout, [this] { return done_; })) { + std::cerr << "FAIL: watchdog timeout — the emitter deadlocked." << std::endl; + std::cerr.flush(); + std::_Exit(1); + } + }); + } + + ~Watchdog() { + { + std::lock_guard lock(mutex_); + done_ = true; + } + cv_.notify_all(); + thread_.join(); + } + + private: + std::mutex mutex_; + std::condition_variable cv_; + bool done_; + std::thread thread_; +}; + +// --------------------------------------------------------------------------- +// Event hierarchy under test +// --------------------------------------------------------------------------- + +class TestEvent : public Event { + public: + explicit TestEvent(int v) : value(v) {} + std::string GetTypeName() const override { return "TestEvent"; } + int value; +}; + +class DerivedEvent : public TestEvent { + public: + explicit DerivedEvent(int v) : TestEvent(v) {} + std::string GetTypeName() const override { return "DerivedEvent"; } +}; + +class OtherEvent : public TestEvent { + public: + explicit OtherEvent(int v) : TestEvent(v) {} + std::string GetTypeName() const override { return "OtherEvent"; } +}; + +// Exposes the protected emit surface for testing. +class TestEmitter : public EventEmitter { + public: + ~TestEmitter() override { ShutdownEmitter(); } + + using EventEmitter::Emit; + + template + void EmitAsyncPublic(Args&&... args) { + EmitAsync(std::forward(args)...); + } + + int start_calls = 0; + int stop_calls = 0; + + protected: + void StartEventListening() override { ++start_calls; } + void StopEventListening() override { ++stop_calls; } +}; + +// --------------------------------------------------------------------------- +// P0-1: EmitAsync must not deadlock on its first call +// --------------------------------------------------------------------------- + +void TestEmitAsyncDoesNotDeadlock() { + std::cout << "[P0-1] EmitAsync first call" << std::endl; + + TestEmitter emitter; + int received = 0; + + emitter.AddListener([&](const TestEvent& e) { received = e.value; }); + + // Before the fix this call never returned: EmitAsync held queue_mutex_ and + // then called StartAsyncProcessing(), which locked queue_mutex_ again. + emitter.EmitAsyncPublic(42); + + Check(received == 0, "EmitAsync defers rather than delivering inline"); + g_main_thread->Drain(); + + Check(received == 42, "async event reaches the listener on the main thread"); +} + +// Mirrors ShortcutManager::Register(): EmitAsync is called while the caller +// still holds its own lock (shortcut_manager.cpp:29 -> :34/:42/:56/:66). +// Deferring is what keeps the listener from running under that lock. +void TestEmitAsyncUnderCallerLock() { + std::cout << "[P0-1] EmitAsync while caller holds its own lock" << std::endl; + + TestEmitter emitter; + std::mutex caller_mutex; + bool ran_under_caller_lock = false; + int received = 0; + + emitter.AddListener([&](const TestEvent& e) { + received = e.value; + // If the callback runs while the emitting scope still holds caller_mutex_, + // try_lock fails — that is the hazard this design avoids. + if (caller_mutex.try_lock()) { + caller_mutex.unlock(); + } else { + ran_under_caller_lock = true; + } + }); + + { + std::unique_lock lock(caller_mutex); + emitter.EmitAsyncPublic(7); + } + g_main_thread->Drain(); + + Check(received == 7, "cross-lock EmitAsync completes"); + Check(!ran_under_caller_lock, "listener does not run under the emitter's caller lock"); +} + +// EmitAsync from a background thread must land on the main thread. +void TestEmitAsyncFromBackgroundThread() { + std::cout << "[P0-5] EmitAsync from a background thread" << std::endl; + + TestEmitter emitter; + std::atomic delivered_on_main{false}; + std::atomic received{0}; + + emitter.AddListener([&](const TestEvent& e) { + received.store(e.value); + delivered_on_main.store(IsMainThread()); + }); + + std::thread worker([&] { emitter.EmitAsyncPublic(99); }); + worker.join(); + + Check(received.load() == 0, "not delivered on the emitting background thread"); + g_main_thread->Drain(); + + Check(received.load() == 99, "event delivered after draining the main thread queue"); + Check(delivered_on_main.load(), "listener ran on the main thread, not the worker"); +} + +// A queued event whose emitter dies before the main thread drains must not +// dispatch into freed memory. +void TestAsyncAfterEmitterDestroyed() { + std::cout << "[P0-5] emitter destroyed before queued event runs" << std::endl; + + std::atomic calls{0}; + { + TestEmitter emitter; + emitter.AddListener([&](const TestEvent&) { ++calls; }); + emitter.EmitAsyncPublic(1); + Check(g_main_thread->PendingCount() == 1, "event is queued while emitter is alive"); + } // ~TestEmitter -> ShutdownEmitter() + + g_main_thread->Drain(); + Check(calls.load() == 0, "queued event is dropped once the emitter is gone"); +} + +// --------------------------------------------------------------------------- +// P0-2: callbacks must run without the listener lock held +// --------------------------------------------------------------------------- + +void TestRemoveSelfFromCallback() { + std::cout << "[P0-2] listener removes itself from its own callback" << std::endl; + + TestEmitter emitter; + std::atomic calls{0}; + size_t id = 0; + + id = emitter.AddListener([&](const TestEvent&) { + ++calls; + // Before the fix this deadlocked: RemoveListener wants listeners_mutex_, + // which Emit was still holding. + emitter.RemoveListener(id); + }); + + emitter.Emit(TestEvent(1)); + emitter.Emit(TestEvent(2)); + + Check(calls.load() == 1, "one-shot listener fires exactly once"); + Check(emitter.GetTotalListenerCount() == 0, "listener is gone after self-removal"); +} + +void TestAddListenerFromCallback() { + std::cout << "[P0-2] listener adds another listener from its callback" << std::endl; + + TestEmitter emitter; + std::atomic outer{0}; + std::atomic inner{0}; + + emitter.AddListener([&](const TestEvent&) { + ++outer; + if (outer.load() == 1) { + emitter.AddListener([&](const TestEvent&) { ++inner; }); + } + }); + + emitter.Emit(TestEvent(1)); + Check(outer.load() == 1, "first emit reaches the original listener"); + Check(inner.load() == 0, "listener added during dispatch does not fire for that same event"); + + emitter.Emit(TestEvent(2)); + Check(inner.load() == 1, "newly added listener fires on the next event"); +} + +void TestReentrantEmitFromCallback() { + std::cout << "[P0-2] re-entrant Emit from inside a callback" << std::endl; + + TestEmitter emitter; + std::atomic depth{0}; + std::atomic max_depth{0}; + + emitter.AddListener([&](const TestEvent& e) { + const int d = ++depth; + if (d > max_depth.load()) { + max_depth.store(d); + } + if (e.value > 0) { + emitter.Emit(TestEvent(e.value - 1)); + } + --depth; + }); + + emitter.Emit(TestEvent(3)); + + Check(max_depth.load() == 4, "nested Emit recursion completes without deadlock"); +} + +void TestRemoveOtherListenerDuringDispatch() { + std::cout << "[P0-2] listener removes a not-yet-invoked listener" << std::endl; + + TestEmitter emitter; + std::atomic second_calls{0}; + size_t second_id = 0; + + emitter.AddListener([&](const TestEvent&) { emitter.RemoveListener(second_id); }); + second_id = emitter.AddListener([&](const TestEvent&) { ++second_calls; }); + + emitter.Emit(TestEvent(1)); + + // The tombstone must suppress delivery even though the snapshot was taken + // before the removal happened. + Check(second_calls.load() == 0, "removed-mid-dispatch listener does not fire"); +} + +// --------------------------------------------------------------------------- +// Dispatch semantics +// --------------------------------------------------------------------------- + +void TestBaseAndDerivedDispatch() { + std::cout << "[dispatch] base/derived routing" << std::endl; + + TestEmitter emitter; + std::atomic base_calls{0}; + std::atomic derived_calls{0}; + std::atomic other_calls{0}; + + emitter.AddListener([&](const TestEvent&) { ++base_calls; }); + emitter.AddListener([&](const DerivedEvent&) { ++derived_calls; }); + emitter.AddListener([&](const OtherEvent&) { ++other_calls; }); + + emitter.Emit(DerivedEvent(1)); + + Check(base_calls.load() == 1, "base listener receives derived event"); + Check(derived_calls.load() == 1, "derived listener receives derived event"); + Check(other_calls.load() == 0, "sibling listener does not receive derived event"); + + emitter.Emit(TestEvent(2)); + + Check(base_calls.load() == 2, "base listener receives base event"); + Check(derived_calls.load() == 1, "derived listener does not receive base event"); +} + +void TestDispatchOrder() { + std::cout << "[dispatch] registration order" << std::endl; + + TestEmitter emitter; + std::vector order; + + emitter.AddListener([&](const TestEvent&) { order.push_back(1); }); + emitter.AddListener([&](const DerivedEvent&) { order.push_back(2); }); + emitter.AddListener([&](const TestEvent&) { order.push_back(3); }); + + emitter.Emit(DerivedEvent(0)); + + const bool ok = order.size() == 3 && order[0] == 1 && order[1] == 2 && order[2] == 3; + Check(ok, "listeners fire in registration order across types"); +} + +void TestDispatchCacheInvalidation() { + std::cout << "[dispatch] cache invalidation" << std::endl; + + TestEmitter emitter; + std::atomic calls{0}; + + emitter.Emit(TestEvent(1)); // Populate the cache with an empty result. + + emitter.AddListener([&](const TestEvent&) { ++calls; }); + emitter.Emit(TestEvent(2)); + Check(calls.load() == 1, "cache invalidated after AddListener"); + + emitter.RemoveAllListeners(); + emitter.Emit(TestEvent(3)); + Check(calls.load() == 1, "cache invalidated after RemoveAllListeners"); +} + +// --------------------------------------------------------------------------- +// Start/StopEventListening lifecycle +// --------------------------------------------------------------------------- + +void TestListeningLifecycle() { + std::cout << "[lifecycle] Start/StopEventListening transitions" << std::endl; + + TestEmitter emitter; + + const size_t a = emitter.AddListener([](const TestEvent&) {}); + Check(emitter.start_calls == 1, "StartEventListening on 0->1"); + + const size_t b = emitter.AddListener([](const TestEvent&) {}); + Check(emitter.start_calls == 1, "no extra StartEventListening on 1->2"); + + emitter.RemoveListener(a); + Check(emitter.stop_calls == 0, "no StopEventListening while listeners remain"); + + emitter.RemoveListener(b); + Check(emitter.stop_calls == 1, "StopEventListening on 1->0"); +} + +// A platform hook that calls back into the emitter must not deadlock. Before the +// fix these hooks ran while listeners_mutex_ was held. +class ReentrantHookEmitter : public EventEmitter { + public: + ~ReentrantHookEmitter() override { ShutdownEmitter(); } + using EventEmitter::Emit; + std::atomic hook_observed_count{0}; + + protected: + void StartEventListening() override { + // Re-enters the emitter from inside the hook. + hook_observed_count.store(static_cast(GetTotalListenerCount())); + } +}; + +void TestHookMayReenter() { + std::cout << "[lifecycle] platform hook re-enters the emitter" << std::endl; + + ReentrantHookEmitter emitter; + emitter.AddListener([](const TestEvent&) {}); + + Check(emitter.hook_observed_count.load() == 1, + "StartEventListening can query the emitter without deadlocking"); +} + +// --------------------------------------------------------------------------- +// Concurrency +// --------------------------------------------------------------------------- + +void TestConcurrentAddRemoveEmit() { + std::cout << "[concurrency] parallel add/remove/emit" << std::endl; + + TestEmitter emitter; + std::atomic stop{false}; + std::atomic deliveries{0}; + + std::thread emitter_thread([&] { + while (!stop.load()) { + emitter.Emit(TestEvent(1)); + } + }); + + std::vector churn; + for (int t = 0; t < 4; ++t) { + churn.emplace_back([&] { + for (int i = 0; i < 200; ++i) { + const size_t id = emitter.AddListener([&](const TestEvent&) { ++deliveries; }); + std::this_thread::yield(); + emitter.RemoveListener(id); + } + }); + } + + for (auto& t : churn) { + t.join(); + } + stop.store(true); + emitter_thread.join(); + + Check(true, "no deadlock or crash under concurrent churn"); + Check(emitter.GetTotalListenerCount() == 0, "all listeners removed after churn"); +} + +int RunTests() { + TestEmitAsyncDoesNotDeadlock(); + TestEmitAsyncUnderCallerLock(); + TestEmitAsyncFromBackgroundThread(); + TestAsyncAfterEmitterDestroyed(); + TestRemoveSelfFromCallback(); + TestAddListenerFromCallback(); + TestReentrantEmitFromCallback(); + TestRemoveOtherListenerDuringDispatch(); + TestBaseAndDerivedDispatch(); + TestDispatchOrder(); + TestDispatchCacheInvalidation(); + TestListeningLifecycle(); + TestHookMayReenter(); + TestConcurrentAddRemoveEmit(); + + if (g_failures != 0) { + std::cerr << g_failures << " check(s) failed." << std::endl; + return 1; + } + std::cout << "All event_emitter checks passed." << std::endl; + return 0; +} + +} // namespace + +int main() { + Watchdog watchdog(std::chrono::seconds(60)); + + FakeMainThread main_thread; + g_main_thread = &main_thread; + + const int result = RunTests(); + + g_main_thread = nullptr; + return result; +} diff --git a/packages/cnativeapi/cxx_impl/tests/handle_table_test.cpp b/packages/cnativeapi/cxx_impl/tests/handle_table_test.cpp new file mode 100644 index 0000000..2ce0aa9 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/handle_table_test.cpp @@ -0,0 +1,333 @@ +// Tests for the generational handle table. +// +// Maps to DESIGN_REVIEW.md P0-3 / P0-4 and docs/handle-ownership.md. The +// behaviours asserted here are exactly the ones raw-pointer handles could not +// provide: stale handles failing safely instead of dereferencing freed memory, +// double-release being a no-op, and handle confusion being rejected rather than +// reinterpreted. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/foundation/handle_table.h" + +// --------------------------------------------------------------------------- +// Test-local types +// +// Registered in the high end of the tag range so they cannot collide with the +// real registry in id_allocator.h, which is append-only for shipping types. +// --------------------------------------------------------------------------- +namespace nativeapi { + +struct FakeWidget { + explicit FakeWidget(int v) : value(v) {} + int value; +}; + +struct FakeGadget { + explicit FakeGadget(int v) : value(v) {} + int value; +}; + +/// Calls back into the table from its destructor. +struct SelfReleasingThing { + std::function on_destroy; + ~SelfReleasingThing() { + if (on_destroy) { + on_destroy(); + } + } +}; + +template <> +struct IdTypeTag { + static constexpr uint32_t value = 200; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 201; +}; +template <> +struct IdTypeTag { + static constexpr uint32_t value = 202; +}; + +} // namespace nativeapi + +namespace { + +using namespace nativeapi; + +int g_failures = 0; + +void Check(bool condition, const std::string& what) { + if (!condition) { + std::cerr << "FAIL: " << what << std::endl; + ++g_failures; + } else { + std::cout << " ok: " << what << std::endl; + } +} + +HandleTable& Table() { + return HandleTable::GetInstance(); +} + +// --------------------------------------------------------------------------- +// Basics +// --------------------------------------------------------------------------- + +void TestInsertResolveRoundTrip() { + std::cout << "[basic] insert/resolve round-trip" << std::endl; + + const auto handle = Table().Insert(std::make_shared(42)); + Check(handle != kInvalidHandle, "insert returns a usable handle"); + + auto resolved = Table().Resolve(handle); + Check(resolved != nullptr, "handle resolves"); + Check(resolved && resolved->value == 42, "resolved object carries the right state"); + Check(Table().Contains(handle), "Contains agrees"); + + Table().Release(handle); +} + +void TestNullAndInvalid() { + std::cout << "[basic] null and invalid inputs" << std::endl; + + Check(Table().Insert(std::shared_ptr()) == kInvalidHandle, + "inserting null yields kInvalidHandle"); + Check(Table().Resolve(kInvalidHandle) == nullptr, "kInvalidHandle never resolves"); + Check(!Table().Release(kInvalidHandle), "releasing kInvalidHandle is a no-op"); + Check(!Table().Contains(kInvalidHandle), "kInvalidHandle is not contained"); + + // A handle whose slot index is far beyond the table. + const HandleValue bogus = HandleTable::Encode(1, 0xFFFFFF); + Check(Table().Resolve(bogus) == nullptr, "out-of-range slot fails safely"); + Check(!Table().Release(bogus), "releasing an out-of-range slot is a no-op"); +} + +// --------------------------------------------------------------------------- +// The failure modes raw pointers could not survive +// --------------------------------------------------------------------------- + +void TestReleaseInvalidatesHandle() { + std::cout << "[safety] release invalidates the handle" << std::endl; + + const auto handle = Table().Insert(std::make_shared(1)); + Check(Table().Release(handle), "first release succeeds"); + + Check(Table().Resolve(handle) == nullptr, "released handle no longer resolves"); + Check(!Table().Contains(handle), "released handle is not contained"); +} + +void TestDoubleReleaseIsSafe() { + std::cout << "[safety] double release" << std::endl; + + const auto handle = Table().Insert(std::make_shared(1)); + + Check(Table().Release(handle), "first release succeeds"); + Check(!Table().Release(handle), "second release reports failure instead of double-freeing"); + Check(!Table().Release(handle), "third release likewise"); +} + +// The critical one for GC languages: a finalizer running late must not be able +// to reach a new object that happens to have landed in the recycled slot. +void TestStaleHandleAfterSlotReuse() { + std::cout << "[safety] stale handle after slot reuse" << std::endl; + + const auto first = Table().Insert(std::make_shared(111)); + const uint32_t slot = HandleTable::SlotOf(first); + Table().Release(first); + + // Force reuse of the same slot. + const auto second = Table().Insert(std::make_shared(222)); + Check(HandleTable::SlotOf(second) == slot, "slot was recycled (precondition)"); + Check(HandleTable::GenerationOf(second) != HandleTable::GenerationOf(first), + "generation advanced on reuse"); + + Check(Table().Resolve(first) == nullptr, + "the OLD handle does not resolve to the NEW occupant"); + + auto live = Table().Resolve(second); + Check(live && live->value == 222, "the new handle resolves correctly"); + + Check(!Table().Release(first), "releasing the stale handle does not evict the new occupant"); + Check(Table().Contains(second), "new occupant survives the stale release"); + + Table().Release(second); +} + +void TestTypeConfusionRejected() { + std::cout << "[safety] handle confusion" << std::endl; + + const auto widget = Table().Insert(std::make_shared(7)); + + Check(Table().Resolve(widget) == nullptr, + "resolving a widget handle as a gadget returns null"); + Check(Table().Resolve(widget) != nullptr, "correct type still resolves"); + Check(Table().GetTypeTag(widget) == IdTypeTag::value, "type tag is readable"); + + Table().Release(widget); +} + +// Resolve() hands out a strong reference; releasing the handle must not pull the +// object out from under a caller that is still using it. +void TestResolvedReferenceOutlivesRelease() { + std::cout << "[safety] resolved reference outlives release" << std::endl; + + auto handle = Table().Insert(std::make_shared(99)); + auto strong = Table().Resolve(handle); + + Table().Release(handle); + + Check(strong != nullptr, "previously resolved reference is still held"); + Check(strong->value == 99, "and the object is still readable after release"); + Check(strong.use_count() == 1, "caller now holds the only reference"); +} + +// Release() must drop the last reference outside its own lock, or a destructor +// touching the table deadlocks. +void TestDestructorMayReenterTable() { + std::cout << "[safety] destructor re-enters the table" << std::endl; + + const auto inner = Table().Insert(std::make_shared(5)); + + auto thing = std::make_shared(); + thing->on_destroy = [inner] { HandleTable::GetInstance().Release(inner); }; + const auto outer = Table().Insert(thing); + thing.reset(); + + // Before deferring destruction past the lock, this call deadlocked. + Check(Table().Release(outer), "releasing an object whose destructor re-enters succeeds"); + Check(!Table().Contains(inner), "the nested release took effect"); +} + +// --------------------------------------------------------------------------- +// Bookkeeping +// --------------------------------------------------------------------------- + +void TestSlotReuseKeepsTableCompact() { + std::cout << "[bookkeeping] slot reuse" << std::endl; + + const size_t baseline = Table().LiveCount(); + + std::vector handles; + for (int i = 0; i < 100; ++i) { + handles.push_back(Table().Insert(std::make_shared(i))); + } + Check(Table().LiveCount() == baseline + 100, "LiveCount tracks insertions"); + + for (const auto handle : handles) { + Table().Release(handle); + } + Check(Table().LiveCount() == baseline, "LiveCount returns to baseline after releases"); + + std::set reused_slots; + std::vector again; + for (int i = 0; i < 100; ++i) { + const auto handle = Table().Insert(std::make_shared(i)); + again.push_back(handle); + reused_slots.insert(HandleTable::SlotOf(handle)); + } + Check(reused_slots.size() == 100, "reinsertion reuses freed slots rather than growing"); + + for (const auto handle : again) { + Table().Release(handle); + } +} + +void TestHandlesAreDistinct() { + std::cout << "[bookkeeping] handle uniqueness" << std::endl; + + std::set seen; + std::vector handles; + for (int i = 0; i < 500; ++i) { + const auto handle = Table().Insert(std::make_shared(i)); + handles.push_back(handle); + seen.insert(handle); + } + + Check(seen.size() == 500, "concurrently live handles are all distinct"); + + for (const auto handle : handles) { + Table().Release(handle); + } +} + +// --------------------------------------------------------------------------- +// Concurrency +// --------------------------------------------------------------------------- + +void TestConcurrentInsertResolveRelease() { + std::cout << "[concurrency] parallel insert/resolve/release" << std::endl; + + constexpr int kThreads = 8; + constexpr int kPerThread = 400; + + const size_t baseline = Table().LiveCount(); + std::atomic resolve_failures{0}; + std::atomic stale_resolves{0}; + + std::vector threads; + for (int t = 0; t < kThreads; ++t) { + threads.emplace_back([&] { + for (int i = 0; i < kPerThread; ++i) { + const auto handle = Table().Insert(std::make_shared(i)); + + auto resolved = Table().Resolve(handle); + if (!resolved || resolved->value != i) { + ++resolve_failures; + } + + Table().Release(handle); + + // Must never resolve after our own release, no matter what other + // threads are doing to that slot. + if (Table().Resolve(handle) != nullptr) { + ++stale_resolves; + } + } + }); + } + for (auto& thread : threads) { + thread.join(); + } + + Check(resolve_failures.load() == 0, "every handle resolved to its own object"); + Check(stale_resolves.load() == 0, "no handle resolved after being released"); + Check(Table().LiveCount() == baseline, "no handles leaked under concurrency"); +} + +int RunTests() { + TestInsertResolveRoundTrip(); + TestNullAndInvalid(); + TestReleaseInvalidatesHandle(); + TestDoubleReleaseIsSafe(); + TestStaleHandleAfterSlotReuse(); + TestTypeConfusionRejected(); + TestResolvedReferenceOutlivesRelease(); + TestDestructorMayReenterTable(); + TestSlotReuseKeepsTableCompact(); + TestHandlesAreDistinct(); + TestConcurrentInsertResolveRelease(); + + if (g_failures != 0) { + std::cerr << g_failures << " check(s) failed." << std::endl; + return 1; + } + std::cout << "All handle_table checks passed." << std::endl; + return 0; +} + +} // namespace + +int main() { + return RunTests(); +} diff --git a/packages/cnativeapi/cxx_impl/tests/id_allocator_test.cpp b/packages/cnativeapi/cxx_impl/tests/id_allocator_test.cpp new file mode 100644 index 0000000..1d4b7a0 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/id_allocator_test.cpp @@ -0,0 +1,194 @@ +// Tests for IdAllocator's ID encoding and type tagging. +// +// Maps to DESIGN_REVIEW.md P1-5: type tags used to be handed out by a runtime +// counter on a first-call-wins basis, so a given C++ type could receive a +// different tag depending on call order or from one run to the next. That made +// the type bits useless for the handle validation they are meant to support. + +#include +#include +#include +#include +#include +#include +#include + +// Only id_allocator.h is needed: Window/Menu/MenuItem/TrayIcon/Display/Shortcut +// are used purely as template arguments, and the tag registry already +// forward-declares them. +#include "../src/foundation/id_allocator.h" + +namespace { + +using namespace nativeapi; + +int g_failures = 0; + +void Check(bool condition, const std::string& what) { + if (!condition) { + std::cerr << "FAIL: " << what << std::endl; + ++g_failures; + } else { + std::cout << " ok: " << what << std::endl; + } +} + +// --------------------------------------------------------------------------- +// Type tags +// --------------------------------------------------------------------------- + +void TestTypeTagsAreCompileTimeConstants() { + std::cout << "[tags] tags are compile-time constants" << std::endl; + + // If these were still runtime-assigned, they could not appear in a constant + // expression at all — this block failing to compile IS the regression test. + static_assert(IdTypeTag::value == 1, "Window tag changed"); + static_assert(IdTypeTag::value == 2, "Menu tag changed"); + static_assert(IdTypeTag::value == 3, "MenuItem tag changed"); + static_assert(IdTypeTag::value == 4, "TrayIcon tag changed"); + + Check(true, "type tags usable in constant expressions"); +} + +void TestTypeTagsAreDistinct() { + std::cout << "[tags] tags are distinct" << std::endl; + + const std::set tags = {IdTypeTag::value, IdTypeTag::value, + IdTypeTag::value, IdTypeTag::value, + IdTypeTag::value, IdTypeTag::value}; + + Check(tags.size() == 6, "all six registered types have distinct tags"); + Check(tags.find(IdAllocator::kInvalidId) == tags.end(), "no type reuses the invalid-ID value"); +} + +// The property the old implementation could not provide: the tag encoded into +// an ID depends only on the type, never on which type allocated first. +void TestEncodedTypeIsIndependentOfAllocationOrder() { + std::cout << "[tags] encoded type is independent of allocation order" << std::endl; + + const auto tray_first = IdAllocator::Allocate(); + const auto window_second = IdAllocator::Allocate(); + + Check(IdAllocator::GetType(tray_first) == IdTypeTag::value, + "TrayIcon ID carries the TrayIcon tag even when allocated first"); + Check(IdAllocator::GetType(window_second) == IdTypeTag::value, + "Window ID carries the Window tag even when allocated second"); +} + +// --------------------------------------------------------------------------- +// ID encoding +// --------------------------------------------------------------------------- + +void TestIdEncoding() { + std::cout << "[encoding] type/sequence round-trip" << std::endl; + + const auto id = IdAllocator::Allocate(); + const auto decomposed = IdAllocator::Decompose(id); + + Check(IdAllocator::IsValid(id), "allocated ID is valid"); + Check(decomposed.first == IdTypeTag::value, "Decompose returns the right type"); + Check(decomposed.second == IdAllocator::GetSequence(id), "Decompose agrees with GetSequence"); + Check(IdAllocator::GetSequence(id) != 0, "sequence is never 0"); + Check(!IdAllocator::IsValid(IdAllocator::kInvalidId), "kInvalidId is not valid"); +} + +void TestIdsAreUniquePerType() { + std::cout << "[encoding] uniqueness within a type" << std::endl; + + std::set ids; + for (int i = 0; i < 1000; ++i) { + ids.insert(IdAllocator::Allocate()); + } + + Check(ids.size() == 1000, "1000 allocations produce 1000 distinct IDs"); +} + +void TestIdsDoNotCollideAcrossTypes() { + std::cout << "[encoding] no collisions across types" << std::endl; + + std::set ids; + bool collision = false; + for (int i = 0; i < 200; ++i) { + if (!ids.insert(IdAllocator::Allocate()).second) + collision = true; + if (!ids.insert(IdAllocator::Allocate()).second) + collision = true; + if (!ids.insert(IdAllocator::Allocate()).second) + collision = true; + } + + Check(!collision, "interleaved allocation across three types never collides"); +} + +void TestIsValidTypeRange() { + std::cout << "[encoding] type range" << std::endl; + + Check(!IdAllocator::IsValidType(0), "0 is not a valid type"); + Check(IdAllocator::IsValidType(1), "1 is a valid type"); + Check(IdAllocator::IsValidType(255), "255 is a valid type — the old cap of 10 is gone"); + Check(!IdAllocator::IsValidType(256), "256 exceeds the 8-bit field"); +} + +// --------------------------------------------------------------------------- +// Concurrency +// --------------------------------------------------------------------------- + +void TestConcurrentAllocationIsUnique() { + std::cout << "[concurrency] parallel allocation" << std::endl; + + constexpr int kThreads = 8; + constexpr int kPerThread = 500; + + std::vector> per_thread(kThreads); + std::vector threads; + + for (int t = 0; t < kThreads; ++t) { + threads.emplace_back([&per_thread, t, kPerThread] { + per_thread[t].reserve(kPerThread); + for (int i = 0; i < kPerThread; ++i) { + per_thread[t].push_back(IdAllocator::Allocate()); + } + }); + } + for (auto& thread : threads) { + thread.join(); + } + + std::set all; + bool wrong_type = false; + for (const auto& chunk : per_thread) { + for (const auto id : chunk) { + all.insert(id); + if (IdAllocator::GetType(id) != IdTypeTag::value) { + wrong_type = true; + } + } + } + + Check(all.size() == kThreads * kPerThread, "concurrent allocations are all distinct"); + Check(!wrong_type, "every concurrently allocated ID carries the correct type tag"); +} + +int RunTests() { + TestTypeTagsAreCompileTimeConstants(); + TestTypeTagsAreDistinct(); + TestEncodedTypeIsIndependentOfAllocationOrder(); + TestIdEncoding(); + TestIdsAreUniquePerType(); + TestIdsDoNotCollideAcrossTypes(); + TestIsValidTypeRange(); + TestConcurrentAllocationIsUnique(); + + if (g_failures != 0) { + std::cerr << g_failures << " check(s) failed." << std::endl; + return 1; + } + std::cout << "All id_allocator checks passed." << std::endl; + return 0; +} + +} // namespace + +int main() { + return RunTests(); +} diff --git a/packages/cnativeapi/cxx_impl/tests/shortcut_accelerator_test.cpp b/packages/cnativeapi/cxx_impl/tests/shortcut_accelerator_test.cpp new file mode 100644 index 0000000..7b69bf3 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/shortcut_accelerator_test.cpp @@ -0,0 +1,145 @@ +// Tests for ShortcutManager::IsValidAccelerator(). +// +// This validator gates every Register() call, so anything it rejects is +// unreachable no matter what the platform backends support. It used to be +// narrower than the backends in ways that silently removed working keys: +// "Return", "Esc", "Control" and "Command" were all parsed by macOS, Windows +// and Linux but refused here, and no punctuation key could be expressed at all +// (so Cmd+Comma -- the standard Preferences shortcut -- was unrepresentable). +// +// The validator is pure, so these cases run identically on every platform. +// Whether a given accelerator can actually be grabbed is a platform and +// runtime question and is not asserted here. + +#include +#include +#include +#include + +#include "../src/shortcut_manager.h" + +namespace { + +using namespace nativeapi; + +int g_failures = 0; + +void Check(bool condition, const std::string& what) { + if (!condition) { + std::cerr << "FAIL: " << what << std::endl; + ++g_failures; + } else { + std::cout << " ok: " << what << std::endl; + } +} + +void ExpectValid(const std::string& accelerator) { + Check(ShortcutManager::GetInstance().IsValidAccelerator(accelerator), + "accepts \"" + accelerator + "\""); +} + +void ExpectInvalid(const std::string& accelerator) { + Check(!ShortcutManager::GetInstance().IsValidAccelerator(accelerator), + "rejects \"" + accelerator + "\""); +} + +// --------------------------------------------------------------------------- +// Modifiers +// --------------------------------------------------------------------------- + +void TestModifierSpellings() { + // Every spelling the platform parsers accept must pass validation too. + for (const char* mod : {"Ctrl", "Control", "Alt", "Option", "Shift", "Cmd", "Command", "Super", + "Meta", "CmdOrCtrl", "CommandOrControl"}) { + ExpectValid(std::string(mod) + "+A"); + } + + ExpectValid("Ctrl+Shift+Alt+Cmd+A"); // stacked + ExpectValid("ctrl+shift+a"); // case-insensitive + ExpectInvalid("Hyper+A"); // unknown modifier +} + +// --------------------------------------------------------------------------- +// Keys +// --------------------------------------------------------------------------- + +void TestLettersDigitsAndFunctionKeys() { + ExpectValid("Ctrl+A"); + ExpectValid("Ctrl+z"); + ExpectValid("Ctrl+0"); + ExpectValid("Ctrl+9"); + + ExpectValid("Ctrl+F1"); + ExpectValid("Ctrl+F9"); + ExpectValid("Ctrl+F10"); + ExpectValid("Ctrl+F12"); + ExpectValid("Ctrl+F24"); + ExpectInvalid("Ctrl+F0"); + ExpectInvalid("Ctrl+F25"); +} + +void TestNamedKeys() { + for (const char* key : {"Space", "Tab", "Enter", "Return", "Escape", "Esc", "Backspace", + "Delete", "ForwardDelete", "Insert", "Help", "Home", "End", "PageUp", + "PageDown", "Up", "Down", "Left", "Right"}) { + ExpectValid(std::string("Ctrl+") + key); + } +} + +void TestPunctuationKeys() { + // By name... + for (const char* key : {"Plus", "Minus", "Equal", "Comma", "Period", "Slash", "Backslash", + "Semicolon", "Quote", "LeftBracket", "RightBracket", "Grave", + "Backquote"}) { + ExpectValid(std::string("Ctrl+") + key); + } + + // ...and by the literal character each name stands for. + for (const char* key : {",", ".", "/", "\\", ";", "'", "[", "]", "`", "=", "-"}) { + ExpectValid(std::string("Ctrl+") + key); + } + + // The motivating case: Preferences on macOS. + ExpectValid("Cmd+Comma"); + ExpectValid("Cmd+,"); +} + +void TestKeypadKeys() { + for (const char* key : {"Num0", "Num5", "Num9", "NumDec", "NumAdd", "NumSub", "NumMult", + "NumDiv", "NumEnter"}) { + ExpectValid(std::string("Ctrl+") + key); + } +} + +// --------------------------------------------------------------------------- +// Malformed input +// --------------------------------------------------------------------------- + +void TestMalformedAccelerators() { + ExpectInvalid(""); + ExpectInvalid("Ctrl+"); // modifier with no key + ExpectInvalid("Ctrl++"); // '+' is spelled "Plus" + ExpectInvalid("Invalid"); // unknown key name + ExpectInvalid("Ctrl+A+B"); // two keys + ExpectInvalid("Ctrl Shift A"); // wrong separator +} + +} // namespace + +int main() { + std::cout << "shortcut_accelerator_test" << std::endl; + + TestModifierSpellings(); + TestLettersDigitsAndFunctionKeys(); + TestNamedKeys(); + TestPunctuationKeys(); + TestKeypadKeys(); + TestMalformedAccelerators(); + + if (g_failures > 0) { + std::cerr << g_failures << " check(s) failed" << std::endl; + return EXIT_FAILURE; + } + std::cout << "all checks passed" << std::endl; + return EXIT_SUCCESS; +} diff --git a/packages/cnativeapi/cxx_impl/tests/url_opener_test.cpp b/packages/cnativeapi/cxx_impl/tests/url_opener_test.cpp new file mode 100644 index 0000000..5557149 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/url_opener_test.cpp @@ -0,0 +1,85 @@ +#include +#include + +#include "../src/url_opener.h" + +namespace { + +int RunTests() { + using namespace nativeapi; + + UrlOpener& opener = UrlOpener::GetInstance(); + + // --- CanOpen validation tests --- + + { + if (opener.CanOpen("")) { + std::cerr << "Expected CanOpen to return false for empty URL." << std::endl; + return 1; + } + } + + { + if (opener.CanOpen("example.com")) { + std::cerr << "Expected CanOpen to return false for missing scheme." << std::endl; + return 1; + } + } + + { + if (opener.CanOpen("mailto:test@example.com")) { + std::cerr << "Expected CanOpen to return false for unsupported scheme." << std::endl; + return 1; + } + } + + { + if (!opener.CanOpen("https://example.com")) { + std::cerr << "Expected CanOpen to return true for a valid https URL." << std::endl; + return 1; + } + } + + { + if (!opener.CanOpen("http://example.com")) { + std::cerr << "Expected CanOpen to return true for a valid http URL." << std::endl; + return 1; + } + } + + // --- Open validation error tests --- + + { + UrlOpenResult result = opener.Open(""); + if (result.success || result.error_code != UrlOpenErrorCode::kInvalidUrlEmpty) { + std::cerr << "Expected Open('') to fail with kInvalidUrlEmpty." << std::endl; + return 1; + } + } + + { + UrlOpenResult result = opener.Open("example.com"); + if (result.success || result.error_code != UrlOpenErrorCode::kInvalidUrlMissingScheme) { + std::cerr << "Expected Open('example.com') to fail with kInvalidUrlMissingScheme." + << std::endl; + return 1; + } + } + + { + UrlOpenResult result = opener.Open("mailto:test@example.com"); + if (result.success || result.error_code != UrlOpenErrorCode::kInvalidUrlUnsupportedScheme) { + std::cerr << "Expected Open('mailto:...') to fail with kInvalidUrlUnsupportedScheme." + << std::endl; + return 1; + } + } + + return 0; +} + +} // namespace + +int main() { + return RunTests(); +} diff --git a/packages/cnativeapi/cxx_impl/tests/window_manager_hook_test.cpp b/packages/cnativeapi/cxx_impl/tests/window_manager_hook_test.cpp new file mode 100644 index 0000000..aafc616 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/tests/window_manager_hook_test.cpp @@ -0,0 +1,182 @@ +// Unit tests for WindowManager will-show/hide/close hook infrastructure. +// +// These tests verify the hook storage and dispatch logic without requiring +// a real platform window. The swizzle-based interception is exercised by +// the examples and integration tests on each platform. + +#include +#include +#include +#include +#include + +#include "../src/window_manager.h" +#include "../src/window.h" + +namespace { + +using namespace nativeapi; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +// Setting a hook should make HasWillCloseHook return true; clearing it +// (passing nullopt) should make it return false. +bool test_set_and_clear_will_close_hook() { + auto& manager = WindowManager::GetInstance(); + + // Clear any existing hook + manager.SetWillCloseHook(std::nullopt); + if (manager.HasWillCloseHook()) { + std::cerr << "FAIL: HasWillCloseHook should be false after clear\n"; + return false; + } + + // Set a hook + manager.SetWillCloseHook([](WindowId) {}); + if (!manager.HasWillCloseHook()) { + std::cerr << "FAIL: HasWillCloseHook should be true after set\n"; + return false; + } + + // Clear it + manager.SetWillCloseHook(std::nullopt); + if (manager.HasWillCloseHook()) { + std::cerr << "FAIL: HasWillCloseHook should be false after second clear\n"; + return false; + } + + return true; +} + +// HandleWillClose should invoke the registered hook with the correct ID. +bool test_handle_will_close_invokes_hook() { + auto& manager = WindowManager::GetInstance(); + + const WindowId test_id = 42; + std::atomic received_id{0}; + std::atomic was_called{false}; + + manager.SetWillCloseHook([&](WindowId id) { + received_id = id; + was_called = true; + }); + + manager.HandleWillClose(test_id); + + // Give async dispatch a moment (HandleWillClose is synchronous, but + // be defensive in case the platform routes through a dispatcher). + for (int i = 0; i < 100 && !was_called; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + if (!was_called) { + std::cerr << "FAIL: will-close hook was not invoked\n"; + manager.SetWillCloseHook(std::nullopt); + return false; + } + + if (received_id != test_id) { + std::cerr << "FAIL: will-close hook received wrong id: " << received_id + << " expected " << test_id << "\n"; + manager.SetWillCloseHook(std::nullopt); + return false; + } + + // Cleanup + manager.SetWillCloseHook(std::nullopt); + return true; +} + +// HandleWillClose with no hook set should be a no-op (not crash). +bool test_handle_will_close_no_hook_is_safe() { + auto& manager = WindowManager::GetInstance(); + manager.SetWillCloseHook(std::nullopt); + + // Should not crash + manager.HandleWillClose(99); + + return true; +} + +// All three hooks (show, hide, close) should coexist independently. +bool test_all_hooks_coexist() { + auto& manager = WindowManager::GetInstance(); + + std::atomic show_called{false}; + std::atomic hide_called{false}; + std::atomic close_called{false}; + + manager.SetWillShowHook([&](WindowId) { show_called = true; }); + manager.SetWillHideHook([&](WindowId) { hide_called = true; }); + manager.SetWillCloseHook([&](WindowId) { close_called = true; }); + + if (!manager.HasWillShowHook() || !manager.HasWillHideHook() || + !manager.HasWillCloseHook()) { + std::cerr << "FAIL: not all hooks report as set\n"; + manager.SetWillShowHook(std::nullopt); + manager.SetWillHideHook(std::nullopt); + manager.SetWillCloseHook(std::nullopt); + return false; + } + + manager.HandleWillShow(1); + manager.HandleWillHide(1); + manager.HandleWillClose(1); + + for (int i = 0; i < 100 && (!show_called || !hide_called || !close_called); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + if (!show_called || !hide_called || !close_called) { + std::cerr << "FAIL: not all hooks were invoked (show=" << show_called + << " hide=" << hide_called << " close=" << close_called << ")\n"; + manager.SetWillShowHook(std::nullopt); + manager.SetWillHideHook(std::nullopt); + manager.SetWillCloseHook(std::nullopt); + return false; + } + + // Cleanup + manager.SetWillShowHook(std::nullopt); + manager.SetWillHideHook(std::nullopt); + manager.SetWillCloseHook(std::nullopt); + return true; +} + +} // namespace + +int main() { + struct TestCase { + const char* name; + bool (*fn)(); + }; + + TestCase tests[] = { + {"set_and_clear_will_close_hook", test_set_and_clear_will_close_hook}, + {"handle_will_close_invokes_hook", test_handle_will_close_invokes_hook}, + {"handle_will_close_no_hook_is_safe", + test_handle_will_close_no_hook_is_safe}, + {"all_hooks_coexist", test_all_hooks_coexist}, + }; + + int failures = 0; + for (const auto& tc : tests) { + std::cout << "RUN " << tc.name << "\n"; + if (tc.fn()) { + std::cout << "PASS " << tc.name << "\n"; + } else { + std::cout << "FAIL " << tc.name << "\n"; + ++failures; + } + } + + if (failures > 0) { + std::cout << "\n" << failures << " test(s) failed\n"; + return 1; + } + + std::cout << "\nAll tests passed\n"; + return 0; +} From 5453312ade837c0d24cb28152359034b4a5f3971 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sun, 23 Aug 2026 21:17:38 +0200 Subject: [PATCH 3/4] fix(ios): add willClose hook stubs to window_manager_ios iOS had SetWillShowHook/SetWillHideHook stubs but was missing the four willClose methods (SetWillCloseHook, HasWillCloseHook, HandleWillClose, CallOriginalClose) added by the willClose-hook feature branch. This caused undefined-symbol linker errors on iOS builds. iOS has no desktop window close to intercept, so these are no-op stubs matching the existing show/hide pattern. --- .../src/platform/ios/window_manager_ios.mm | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm b/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm index 82e11c1..868bd71 100644 --- a/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm @@ -45,6 +45,10 @@ // Empty implementation } +void WindowManager::SetWillCloseHook(std::optional hook) { + // Empty implementation — iOS has no desktop window close to intercept +} + bool WindowManager::HasWillShowHook() const { return false; } @@ -53,6 +57,10 @@ return false; } +bool WindowManager::HasWillCloseHook() const { + return false; +} + void WindowManager::HandleWillShow(WindowId id) { // Empty implementation } @@ -61,6 +69,10 @@ // Empty implementation } +void WindowManager::HandleWillClose(WindowId id) { + // Empty implementation +} + bool WindowManager::CallOriginalShow(WindowId id) { // iOS doesn't support swizzling for window show/hide // Return false to indicate unsupported @@ -73,6 +85,12 @@ return false; } +bool WindowManager::CallOriginalClose(WindowId id) { + // iOS doesn't support swizzling for window close + // Return false to indicate unsupported + return false; +} + void WindowManager::StartEventListening() { // iOS manages window events through UIKit } From 0b3aafa797dafd3012a4fdb74f403d6cb20288b1 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sun, 23 Aug 2026 22:37:29 +0200 Subject: [PATCH 4/4] fix(macos): dispatch NSWindow UI ops to main thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flutter calls Window::Show(), Focus(), etc. from the io.flutter.ui thread, not the main thread. macOS requires NSWindow ordering/visibility operations on the main thread — calling makeKeyAndOrderFront: from another thread triggers EXC_BREAKPOINT with 'Must only be used from the main thread'. All void methods that touch window ordering, visibility, key state, frame, or dragging now dispatch async to the main queue via RunOnMainThread(). Getters stay synchronous (they read state and don't crash off-main-thread). --- .../src/platform/macos/window_macos.mm | 134 ++++++++++-------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm b/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm index ef8fed7..dae0a6e 100644 --- a/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm @@ -12,6 +12,19 @@ // Key for associated objects (used by both window_macos.mm and window_manager_macos.mm) const void* kWindowIdKey = &kWindowIdKey; +// NSWindow UI operations (ordering, visibility, key state) must run on the main +// thread. Flutter calls these from the UI thread (io.flutter.ui), not the main +// thread, so we dispatch async. The old window_manager package did the same. +// ponytail: async means Show() returns before the window is actually visible; +// acceptable — callers don't depend on synchronous visibility. +static inline void RunOnMainThread(dispatch_block_t block) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), block); + } +} + namespace nativeapi { // Private implementation class @@ -67,11 +80,13 @@ Window::~Window() {} void Window::Focus() { - [pimpl_->ns_window_ makeKeyAndOrderFront:nil]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ [w makeKeyAndOrderFront:nil]; }); } void Window::Blur() { - [pimpl_->ns_window_ orderBack:nil]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ [w orderBack:nil]; }); } bool Window::IsFocused() const { @@ -79,22 +94,31 @@ } void Window::Show() { - [pimpl_->ns_window_ setIsVisible:YES]; - // Panels receive key focus when shown but should not activate the app. - if (![pimpl_->ns_window_ isKindOfClass:[NSPanel class]]) { - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - } - [pimpl_->ns_window_ makeKeyAndOrderFront:nil]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + [w setIsVisible:YES]; + // Panels receive key focus when shown but should not activate the app. + if (![w isKindOfClass:[NSPanel class]]) { + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + } + [w makeKeyAndOrderFront:nil]; + }); } void Window::ShowInactive() { - [pimpl_->ns_window_ setIsVisible:YES]; - [pimpl_->ns_window_ orderFrontRegardless]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + [w setIsVisible:YES]; + [w orderFrontRegardless]; + }); } void Window::Hide() { - [pimpl_->ns_window_ setIsVisible:NO]; - [pimpl_->ns_window_ orderOut:nil]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + [w setIsVisible:NO]; + [w orderOut:nil]; + }); } bool Window::IsVisible() const { @@ -102,15 +126,13 @@ } void Window::Maximize() { - if (!IsMaximized()) { - [pimpl_->ns_window_ zoom:nil]; - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if (![w isZoomed]) [w zoom:nil]; }); } void Window::Unmaximize() { - if (IsMaximized()) { - [pimpl_->ns_window_ zoom:nil]; - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if ([w isZoomed]) [w zoom:nil]; }); } bool Window::IsMaximized() const { @@ -118,15 +140,13 @@ } void Window::Minimize() { - if (!IsMinimized()) { - [pimpl_->ns_window_ miniaturize:nil]; - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if (![w isMiniaturized]) [w miniaturize:nil]; }); } void Window::Restore() { - if (IsMinimized()) { - [pimpl_->ns_window_ deminiaturize:nil]; - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if ([w isMiniaturized]) [w deminiaturize:nil]; }); } bool Window::IsMinimized() const { @@ -134,15 +154,11 @@ } void Window::SetFullScreen(bool is_full_screen) { - if (is_full_screen) { - if (!IsFullScreen()) { - [pimpl_->ns_window_ toggleFullScreen:nil]; - } - } else { - if (IsFullScreen()) { - [pimpl_->ns_window_ toggleFullScreen:nil]; - } - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + bool fs = ([w styleMask] & NSWindowStyleMaskFullScreen) != 0; + if (is_full_screen != fs) [w toggleFullScreen:nil]; + }); } bool Window::IsFullScreen() const { @@ -153,10 +169,10 @@ //// Color Window::GetBackgroundColor() const; void Window::SetBounds(Rectangle bounds) { - // Convert from topLeft coordinate system to bottom-left (macOS default) + NSWindow* w = pimpl_->ns_window_; NSRect topLeftRect = NSMakeRect(bounds.x, bounds.y, bounds.width, bounds.height); NSRect nsRect = NSRectExt::bottomLeft(topLeftRect); - [pimpl_->ns_window_ setFrame:nsRect display:YES]; + RunOnMainThread(^{ [w setFrame:nsRect display:YES]; }); } Rectangle Window::GetBounds() const { @@ -169,15 +185,18 @@ } void Window::SetSize(Size size, bool animate) { - NSRect frame = [pimpl_->ns_window_ frame]; - frame.origin.y += (frame.size.height - size.height); - frame.size.width = size.width; - frame.size.height = size.height; - if (animate) { - [[pimpl_->ns_window_ animator] setFrame:frame display:YES animate:YES]; - } else { - [pimpl_->ns_window_ setFrame:frame display:YES]; - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + NSRect frame = [w frame]; + frame.origin.y += (frame.size.height - size.height); + frame.size.width = size.width; + frame.size.height = size.height; + if (animate) { + [[w animator] setFrame:frame display:YES animate:YES]; + } else { + [w setFrame:frame display:YES]; + } + }); } Size Window::GetSize() const { @@ -338,12 +357,13 @@ } void Window::SetPosition(Point point) { - // Convert from topLeft coordinate system to bottom-left (macOS default) - // We need the window height to correctly convert the top-left position - NSRect frame = [pimpl_->ns_window_ frame]; - CGPoint topLeftPoint = {point.x, point.y}; - NSPoint bottomLeft = NSPointExt::bottomLeftForWindow(topLeftPoint, frame.size.height); - [pimpl_->ns_window_ setFrameOrigin:bottomLeft]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + NSRect frame = [w frame]; + CGPoint topLeftPoint = {point.x, point.y}; + NSPoint bottomLeft = NSPointExt::bottomLeftForWindow(topLeftPoint, frame.size.height); + [w setFrameOrigin:bottomLeft]; + }); } Point Window::GetPosition() const { @@ -355,8 +375,8 @@ } void Window::Center() { - // Use NSWindow's center method which automatically centers on the main screen - [pimpl_->ns_window_ center]; + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ [w center]; }); } void Window::SetTitle(std::string title) { @@ -525,10 +545,12 @@ } void Window::StartDragging() { - NSWindow* window = pimpl_->ns_window_; - if (window.currentEvent) { - [window performWindowDragWithEvent:window.currentEvent]; - } + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + if (w.currentEvent) { + [w performWindowDragWithEvent:w.currentEvent]; + } + }); } void Window::StartResizing() {}