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..868bd71 --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/ios/window_manager_ios.mm @@ -0,0 +1,106 @@ +#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 +} + +void WindowManager::SetWillCloseHook(std::optional hook) { + // Empty implementation — iOS has no desktop window close to intercept +} + +bool WindowManager::HasWillShowHook() const { + return false; +} + +bool WindowManager::HasWillHideHook() const { + return false; +} + +bool WindowManager::HasWillCloseHook() const { + return false; +} + +void WindowManager::HandleWillShow(WindowId id) { + // Empty implementation +} + +void WindowManager::HandleWillHide(WindowId id) { + // 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 + return false; +} + +bool WindowManager::CallOriginalHide(WindowId id) { + // iOS doesn't support swizzling for window show/hide + // Return false to indicate unsupported + 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 +} + +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..dae0a6e --- /dev/null +++ b/packages/cnativeapi/cxx_impl/src/platform/macos/window_macos.mm @@ -0,0 +1,566 @@ +#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; + +// 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 +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() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ [w makeKeyAndOrderFront:nil]; }); +} + +void Window::Blur() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ [w orderBack:nil]; }); +} + +bool Window::IsFocused() const { + return [pimpl_->ns_window_ isKeyWindow]; +} + +void Window::Show() { + 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() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + [w setIsVisible:YES]; + [w orderFrontRegardless]; + }); +} + +void Window::Hide() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + [w setIsVisible:NO]; + [w orderOut:nil]; + }); +} + +bool Window::IsVisible() const { + return [pimpl_->ns_window_ isVisible]; +} + +void Window::Maximize() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if (![w isZoomed]) [w zoom:nil]; }); +} + +void Window::Unmaximize() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if ([w isZoomed]) [w zoom:nil]; }); +} + +bool Window::IsMaximized() const { + return [pimpl_->ns_window_ isZoomed]; +} + +void Window::Minimize() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if (![w isMiniaturized]) [w miniaturize:nil]; }); +} + +void Window::Restore() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ if ([w isMiniaturized]) [w deminiaturize:nil]; }); +} + +bool Window::IsMinimized() const { + return [pimpl_->ns_window_ isMiniaturized]; +} + +void Window::SetFullScreen(bool is_full_screen) { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ + bool fs = ([w styleMask] & NSWindowStyleMaskFullScreen) != 0; + if (is_full_screen != fs) [w 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) { + NSWindow* w = pimpl_->ns_window_; + NSRect topLeftRect = NSMakeRect(bounds.x, bounds.y, bounds.width, bounds.height); + NSRect nsRect = NSRectExt::bottomLeft(topLeftRect); + RunOnMainThread(^{ [w 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) { + 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 { + 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) { + 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 { + 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() { + NSWindow* w = pimpl_->ns_window_; + RunOnMainThread(^{ [w 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* w = pimpl_->ns_window_; + RunOnMainThread(^{ + if (w.currentEvent) { + [w performWindowDragWithEvent:w.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; +} 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