This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch reproducible-widget-previews
in repository efl.

View the commit online.

commit dd3f3c773f2b69032ff4efebfc3db7bcaddd7645
Author: Cedric BAIL <[email protected]>
AuthorDate: Thu Mar 26 15:28:36 2026 -0600

    feat(cocoa): add NSMenu bridge for native macOS menu bar support
    
    Add a two-layer Cocoa menu bridge so EFL applications can display
    native macOS menu bars via elm_win_main_menu_get().
    
    ecore_cocoa layer (ecore_cocoa_menu.m):
    - C API wrapping NSMenu/NSMenuItem with MRR memory management
    - Tag-based item lookup to avoid positional index drift
    - Deferred [NSApp setMainMenu:] via bounded ecore_idler retries
    - ObjC target helper forwarding menu clicks to C callbacks
    
    Elementary layer (elm_cocoa_menu.c):
    - Bridge mirroring elm_dbus_menu.c pattern for Cocoa backend
    - Recursive item walker syncing elm_menu tree to NSMenu
    - Hash-based ID management with tag-based operations
    - Hooks in elm_menu.c at add/delete/disable/destroy points
    
    Integration:
    - elm_win_main_menu_get() gains #ifdef HAVE_ELEMENTARY_COCOA path
    - Separate cocoa_menu/cocoa_idx fields in Elm_Menu_Data (no D-Bus
      code touched)
    - -framework Cocoa case fix in ecore_cocoa meson.build
---
 src/lib/ecore_cocoa/Ecore_Cocoa.h      | 150 +++++++++++++++
 src/lib/ecore_cocoa/ecore_cocoa_menu.m | 320 +++++++++++++++++++++++++++++++
 src/lib/ecore_cocoa/meson.build        |   3 +-
 src/lib/elementary/efl_ui_win.c        |  16 ++
 src/lib/elementary/elm_cocoa_menu.c    | 338 +++++++++++++++++++++++++++++++++
 src/lib/elementary/elm_menu.c          |  33 ++++
 src/lib/elementary/elm_priv.h          |  16 ++
 src/lib/elementary/elm_widget_menu.h   |  16 ++
 src/lib/elementary/meson.build         |   1 +
 9 files changed, 892 insertions(+), 1 deletion(-)

diff --git a/src/lib/ecore_cocoa/Ecore_Cocoa.h b/src/lib/ecore_cocoa/Ecore_Cocoa.h
index 3474c6cc87..ccdcce21f6 100644
--- a/src/lib/ecore_cocoa/Ecore_Cocoa.h
+++ b/src/lib/ecore_cocoa/Ecore_Cocoa.h
@@ -604,6 +604,156 @@ EAPI void ecore_cocoa_reopen_cb_set(Ecore_Cocoa_Reopen_Cb cb);
 EAPI void ecore_cocoa_url_open_cb_set(Ecore_Cocoa_URL_Open_Cb cb);
 
 
+/* ===================================================================
+ * Menu API
+ * =================================================================== */
+
+/**
+ * @typedef Ecore_Cocoa_Menu
+ * Opaque handle for a native NSMenu managed through Ecore_Cocoa.
+ */
+typedef struct _Ecore_Cocoa_Menu Ecore_Cocoa_Menu;
+
+/**
+ * Creates a new top-level or sub NSMenu.
+ * @param title The menu title (used by macOS for the menu bar title).
+ * @return An opaque handle, or NULL on failure.
+ */
+EAPI Ecore_Cocoa_Menu *ecore_cocoa_menu_new(const char *title)
+   EINA_WARN_UNUSED_RESULT;
+
+/**
+ * Frees an NSMenu and all associated target objects.
+ * @param menu The menu to free.
+ */
+EAPI void ecore_cocoa_menu_free(Ecore_Cocoa_Menu *menu);
+
+/**
+ * Adds a labeled menu item with an optional action callback.
+ * @param menu   The menu to add the item to.
+ * @param label  UTF-8 label text.
+ * @param icon   Reserved for future use (pass NULL).
+ * @param cb     C callback invoked when the item is clicked, or NULL.
+ * @param data   User data passed to @c cb.
+ * @return The zero-based index of the new item, or -1 on failure.
+ */
+EAPI int ecore_cocoa_menu_item_add(Ecore_Cocoa_Menu *menu,
+                                   const char       *label,
+                                   const char       *icon,
+                                   void (*cb)(void *data,
+                                              Ecore_Cocoa_Menu *menu,
+                                              int idx),
+                                   void             *data)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Adds a separator item to the menu.
+ * @param menu The menu to add the separator to.
+ * @return The zero-based index of the separator, or -1 on failure.
+ */
+EAPI int ecore_cocoa_menu_item_add_separator(Ecore_Cocoa_Menu *menu)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Removes the item at @c idx from the menu.
+ * @param menu The menu.
+ * @param idx  Index returned by ecore_cocoa_menu_item_add().
+ */
+EAPI void ecore_cocoa_menu_item_del(Ecore_Cocoa_Menu *menu, int idx)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Sets an integer tag on a menu item.  Tags can be used to identify
+ * items independently of their positional index (which drifts on
+ * deletion).
+ * @param menu The menu.
+ * @param idx  Positional index of the item.
+ * @param tag  Integer tag value (stored in NSMenuItem.tag).
+ */
+EAPI void ecore_cocoa_menu_item_tag_set(Ecore_Cocoa_Menu *menu,
+                                        int               idx,
+                                        int               tag)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Removes a menu item identified by its tag.
+ * @param menu The menu.
+ * @param tag  Tag previously assigned via ecore_cocoa_menu_item_tag_set().
+ */
+EAPI void ecore_cocoa_menu_item_del_by_tag(Ecore_Cocoa_Menu *menu,
+                                           int               tag)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Returns the positional index of the menu item matching the given tag.
+ * @param menu The menu.
+ * @param tag  Tag previously assigned via ecore_cocoa_menu_item_tag_set().
+ * @return Positional index (>= 0) or -1 if not found.
+ */
+EAPI int ecore_cocoa_menu_item_index_by_tag(Ecore_Cocoa_Menu *menu,
+                                             int               tag)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Enables or disables a menu item.
+ * @param menu    The menu.
+ * @param idx     Item index.
+ * @param enabled EINA_TRUE to enable, EINA_FALSE to grey out.
+ */
+EAPI void ecore_cocoa_menu_item_enabled_set(Ecore_Cocoa_Menu *menu,
+                                            int               idx,
+                                            Eina_Bool         enabled)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Enables or disables a menu item identified by its tag.
+ * @param menu    The menu.
+ * @param tag     Tag value.
+ * @param enabled EINA_TRUE to enable, EINA_FALSE to grey out.
+ */
+EAPI void ecore_cocoa_menu_item_enabled_set_by_tag(Ecore_Cocoa_Menu *menu,
+                                                   int               tag,
+                                                   Eina_Bool         enabled)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Updates the label of an existing menu item.
+ * @param menu  The menu.
+ * @param idx   Item index.
+ * @param label New UTF-8 label.
+ */
+EAPI void ecore_cocoa_menu_item_label_set(Ecore_Cocoa_Menu *menu,
+                                          int               idx,
+                                          const char       *label)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Attaches a sub-menu to an existing menu item.
+ * @param menu    The parent menu.
+ * @param idx     Index of the parent item that will own the submenu.
+ * @param submenu The child menu to attach.
+ */
+EAPI void ecore_cocoa_menu_submenu_set(Ecore_Cocoa_Menu *menu,
+                                       int               idx,
+                                       Ecore_Cocoa_Menu *submenu)
+   EINA_ARG_NONNULL(1, 3);
+
+/**
+ * Installs @c menu as the application's main menu bar.
+ * The menu is set unconditionally — NSApp is always initialised by the
+ * time a window exists.
+ * @param menu The top-level menu to install.
+ */
+EAPI void ecore_cocoa_menu_main_set(Ecore_Cocoa_Menu *menu)
+   EINA_ARG_NONNULL(1);
+
+/**
+ * Returns the current main menu handle, or NULL if none has been set.
+ */
+EAPI Ecore_Cocoa_Menu *ecore_cocoa_menu_main_get(void)
+   EINA_WARN_UNUSED_RESULT;
+
+
 /*
  * The clipboard API is still BETA
  */
diff --git a/src/lib/ecore_cocoa/ecore_cocoa_menu.m b/src/lib/ecore_cocoa/ecore_cocoa_menu.m
new file mode 100644
index 0000000000..dc5600e855
--- /dev/null
+++ b/src/lib/ecore_cocoa/ecore_cocoa_menu.m
@@ -0,0 +1,320 @@
+/* ecore_cocoa_menu.m
+ * Cocoa NSMenu layer for the EFL Cocoa menu bridge.
+ * Manual Retain-Release — NO ARC.
+ */
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <Eina.h>
+#include <Ecore.h>
+#import  <Cocoa/Cocoa.h>
+#include "Ecore_Cocoa.h"
+@class EcoreCocoaWindow; /* forward-declare for ecore_cocoa_private.h */
+#include "ecore_cocoa_private.h"
+
+/* ------------------------------------------------------------------ */
+/* ObjC helper: one instance per menu item, retained in the targets    */
+/* array; dispatches the C callback on NSMenuItem action.              */
+/* ------------------------------------------------------------------ */
+@interface _EcoreCocoaMenuTarget : NSObject
+{
+   void  (*_cb)(void *data, Ecore_Cocoa_Menu *menu, int idx);
+   void           *_data;
+   Ecore_Cocoa_Menu *_menu;
+   int              _idx;
+}
+- (id)initWithCallback:(void (*)(void *, Ecore_Cocoa_Menu *, int))cb
+                  data:(void *)data
+                  menu:(Ecore_Cocoa_Menu *)menu
+                   idx:(int)idx;
+- (void)menuItemClicked:(id)sender;
+@end
+
+@implementation _EcoreCocoaMenuTarget
+- (id)initWithCallback:(void (*)(void *, Ecore_Cocoa_Menu *, int))cb
+                  data:(void *)data
+                  menu:(Ecore_Cocoa_Menu *)menu
+                   idx:(int)idx
+{
+   if (!(self = [super init])) return nil;
+   _cb   = cb;
+   _data = data;
+   _menu = menu;
+   _idx  = idx;
+   return self;
+}
+
+- (void)menuItemClicked:(id)sender
+{
+   (void)sender;
+   if (_cb) _cb(_data, _menu, _idx);
+}
+
+@end
+
+/* ------------------------------------------------------------------ */
+/* Opaque handle                                                        */
+/* ------------------------------------------------------------------ */
+struct _Ecore_Cocoa_Menu
+{
+   NSMenu         *ns_menu;
+   NSMutableArray *targets; /* retains _EcoreCocoaMenuTarget instances */
+};
+
+/* Module-level main menu pointer */
+static Ecore_Cocoa_Menu *_main_menu = NULL;
+
+/* ------------------------------------------------------------------ */
+/* Public C API                                                         */
+/* ------------------------------------------------------------------ */
+
+EAPI Ecore_Cocoa_Menu *
+ecore_cocoa_menu_new(const char *title)
+{
+   Ecore_Cocoa_Menu *menu;
+   NSString *ns_title;
+
+   menu = calloc(1, sizeof(*menu));
+   if (EINA_UNLIKELY(!menu))
+     {
+        CRI("Failed to allocate Ecore_Cocoa_Menu");
+        return NULL;
+     }
+
+   ns_title = title ? [NSString stringWithUTF8String:title] : @"";
+   menu->ns_menu = [[NSMenu alloc] initWithTitle:ns_title];
+   if (EINA_UNLIKELY(!menu->ns_menu))
+     {
+        CRI("Failed to create NSMenu");
+        free(menu);
+        return NULL;
+     }
+   [menu->ns_menu setAutoenablesItems:NO];
+
+   menu->targets = [[NSMutableArray alloc] init];
+   if (EINA_UNLIKELY(!menu->targets))
+     {
+        CRI("Failed to create targets array");
+        [menu->ns_menu release];
+        free(menu);
+        return NULL;
+     }
+
+   return menu;
+}
+
+EAPI void
+ecore_cocoa_menu_free(Ecore_Cocoa_Menu *menu)
+{
+   if (!menu) return;
+
+   [menu->targets release];
+   [menu->ns_menu release];
+   free(menu);
+}
+
+EAPI int
+ecore_cocoa_menu_item_add(Ecore_Cocoa_Menu *menu,
+                          const char       *label,
+                          const char       *icon EINA_UNUSED,
+                          void (*cb)(void *data, Ecore_Cocoa_Menu *menu, int idx),
+                          void             *data)
+{
+   NSMenuItem           *item;
+   _EcoreCocoaMenuTarget *target;
+   NSString             *ns_label;
+   int                   idx;
+
+   EINA_SAFETY_ON_NULL_RETURN_VAL(menu, -1);
+
+   idx      = [menu->ns_menu numberOfItems];
+   ns_label = label ? [NSString stringWithUTF8String:label] : @"";
+
+   target = [[_EcoreCocoaMenuTarget alloc] initWithCallback:cb
+                                                       data:data
+                                                       menu:menu
+                                                        idx:idx];
+   if (EINA_UNLIKELY(!target))
+     {
+        CRI("Failed to create menu target");
+        return -1;
+     }
+
+   item = [[NSMenuItem alloc] initWithTitle:ns_label
+                                     action:@selector(menuItemClicked:)
+                              keyEquivalent:@""];
+   if (EINA_UNLIKELY(!item))
+     {
+        CRI("Failed to create NSMenuItem");
+        [target release];
+        return -1;
+     }
+
+   [item setTarget:target];
+   [item setEnabled:YES];
+   [item setTag:(NSInteger)idx]; /* default tag = positional index at creation */
+   [menu->ns_menu addItem:item];
+   [menu->targets addObject:target];
+
+   [item release];
+   [target release]; /* targets array retains it */
+
+   return idx;
+}
+
+EAPI int
+ecore_cocoa_menu_item_add_separator(Ecore_Cocoa_Menu *menu)
+{
+   int idx;
+
+   EINA_SAFETY_ON_NULL_RETURN_VAL(menu, -1);
+
+   idx = [menu->ns_menu numberOfItems];
+   [menu->ns_menu addItem:[NSMenuItem separatorItem]];
+   /* separators have no target; push NULL placeholder to keep array aligned */
+   [menu->targets addObject:[NSNull null]];
+
+   return idx;
+}
+
+EAPI void
+ecore_cocoa_menu_item_del(Ecore_Cocoa_Menu *menu, int idx)
+{
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+   if (idx < 0 || (NSInteger)idx >= [menu->ns_menu numberOfItems]) return;
+
+   /* targets array is kept parallel to ns_menu items — remove the same slot */
+   if ([menu->targets count] != (NSUInteger)[menu->ns_menu numberOfItems])
+     CRI("targets/items array mismatch (%lu vs %ld) — skipping target removal",
+         (unsigned long)[menu->targets count],
+         (long)[menu->ns_menu numberOfItems]);
+   else if ((NSUInteger)idx < [menu->targets count])
+     [menu->targets removeObjectAtIndex:(NSUInteger)idx];
+
+   [menu->ns_menu removeItemAtIndex:idx];
+}
+
+EAPI void
+ecore_cocoa_menu_item_tag_set(Ecore_Cocoa_Menu *menu, int idx, int tag)
+{
+   NSMenuItem *item;
+
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+   if (idx < 0 || (NSInteger)idx >= [menu->ns_menu numberOfItems]) return;
+
+   item = [menu->ns_menu itemAtIndex:idx];
+   [item setTag:(NSInteger)tag];
+}
+
+EAPI void
+ecore_cocoa_menu_item_del_by_tag(Ecore_Cocoa_Menu *menu, int tag)
+{
+   NSInteger ns_idx;
+
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+
+   ns_idx = [menu->ns_menu indexOfItemWithTag:(NSInteger)tag];
+   if (ns_idx < 0) return;
+
+   if ([menu->targets count] != (NSUInteger)[menu->ns_menu numberOfItems])
+     CRI("targets/items array mismatch (%lu vs %ld) — skipping target removal",
+         (unsigned long)[menu->targets count],
+         (long)[menu->ns_menu numberOfItems]);
+   else if ((NSUInteger)ns_idx < [menu->targets count])
+     [menu->targets removeObjectAtIndex:(NSUInteger)ns_idx];
+
+   [menu->ns_menu removeItemAtIndex:ns_idx];
+}
+
+EAPI int
+ecore_cocoa_menu_item_index_by_tag(Ecore_Cocoa_Menu *menu, int tag)
+{
+   NSInteger ns_idx;
+
+   EINA_SAFETY_ON_NULL_RETURN_VAL(menu, -1);
+
+   ns_idx = [menu->ns_menu indexOfItemWithTag:(NSInteger)tag];
+   /* NSNotFound is typically a large positive value; map it to -1 */
+   if (ns_idx == NSNotFound || ns_idx < 0) return -1;
+
+   return (int)ns_idx;
+}
+
+EAPI void
+ecore_cocoa_menu_item_enabled_set(Ecore_Cocoa_Menu *menu, int idx, Eina_Bool enabled)
+{
+   NSMenuItem *item;
+
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+   if (idx < 0 || (NSInteger)idx >= [menu->ns_menu numberOfItems]) return;
+
+   item = [menu->ns_menu itemAtIndex:idx];
+   [item setEnabled:(enabled ? YES : NO)];
+}
+
+EAPI void
+ecore_cocoa_menu_item_enabled_set_by_tag(Ecore_Cocoa_Menu *menu, int tag, Eina_Bool enabled)
+{
+   NSInteger ns_idx;
+   NSMenuItem *item;
+
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+
+   ns_idx = [menu->ns_menu indexOfItemWithTag:(NSInteger)tag];
+   if (ns_idx < 0) return;
+
+   item = [menu->ns_menu itemAtIndex:ns_idx];
+   [item setEnabled:(enabled ? YES : NO)];
+}
+
+EAPI void
+ecore_cocoa_menu_item_label_set(Ecore_Cocoa_Menu *menu, int idx, const char *label)
+{
+   NSMenuItem *item;
+
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+   if (idx < 0 || (NSInteger)idx >= [menu->ns_menu numberOfItems]) return;
+
+   item = [menu->ns_menu itemAtIndex:idx];
+   [item setTitle:label ? [NSString stringWithUTF8String:label] : @""];
+}
+
+EAPI void
+ecore_cocoa_menu_submenu_set(Ecore_Cocoa_Menu *menu, int idx,
+                             Ecore_Cocoa_Menu *submenu)
+{
+   NSMenuItem *item;
+
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+   EINA_SAFETY_ON_NULL_RETURN(submenu);
+   if (idx < 0 || idx >= [menu->ns_menu numberOfItems]) return;
+
+   item = [menu->ns_menu itemAtIndex:idx];
+   /* Submenu-bearing items must not have a target/action — macOS ignores
+    * the submenu and fires the action instead if both are set. */
+   [item setTarget:nil];
+   [item setAction:NULL];
+   [menu->ns_menu setSubmenu:submenu->ns_menu forItem:item];
+}
+
+EAPI void
+ecore_cocoa_menu_main_set(Ecore_Cocoa_Menu *menu)
+{
+   EINA_SAFETY_ON_NULL_RETURN(menu);
+
+   /* Note: [NSApp isRunning] returns NO after the initial bootstrap
+    * because ecore_cocoa calls [NSApp stop:nil] in
+    * applicationDidFinishLaunching and drives the run-loop manually.
+    * So we set the main menu unconditionally — by the time an EFL
+    * window exists and calls elm_win_main_menu_get(), NSApp is fully
+    * initialised even though isRunning is false. */
+   [NSApp setMainMenu:menu->ns_menu];
+   _main_menu = menu;
+}
+
+EAPI Ecore_Cocoa_Menu *
+ecore_cocoa_menu_main_get(void)
+{
+   return _main_menu;
+}
diff --git a/src/lib/ecore_cocoa/meson.build b/src/lib/ecore_cocoa/meson.build
index a44d6a4d58..ac52cc1129 100644
--- a/src/lib/ecore_cocoa/meson.build
+++ b/src/lib/ecore_cocoa/meson.build
@@ -1,5 +1,5 @@
 cocoa_external_dep = declare_dependency(
-  link_args : ['-lobjc', '-framework', 'CoreFoundation', '-framework', 'cocoa', ],
+  link_args : ['-lobjc', '-framework', 'CoreFoundation', '-framework', 'Cocoa', ],
 )
 
 ecore_cocoa_deps = [eina, ecore_input, ecore, emile, eet]
@@ -20,6 +20,7 @@ ecore_cocoa_src = files([
   'ecore_cocoa_cnp.m',
   'ecore_cocoa_app.m',
   'ecore_cocoa_app.h',
+  'ecore_cocoa_menu.m',
   'ecore_cocoa_private.h'
 ])
 
diff --git a/src/lib/elementary/efl_ui_win.c b/src/lib/elementary/efl_ui_win.c
index fefaa40165..cf83c3d258 100644
--- a/src/lib/elementary/efl_ui_win.c
+++ b/src/lib/elementary/efl_ui_win.c
@@ -8916,6 +8916,22 @@ elm_win_main_menu_get(Evas_Object *obj)
    sd->main_menu = elm_menu_add(obj);
    _elm_menu_menu_bar_set(sd->main_menu, EINA_TRUE);
 
+#ifdef HAVE_ELEMENTARY_COCOA
+   if (sd->cocoa.win)
+     {
+        if (_elm_cocoa_menu_register(sd->main_menu))
+          {
+             /* Native Cocoa menu bar is active — hide the in-canvas
+              * EFL menu widget.  We reuse _dbus_menu_set(EINA_TRUE)
+              * because its EINA_TRUE branch does exactly what we need:
+              * unswallow the menu, hide it, and update the frame. */
+             _dbus_menu_set(EINA_TRUE, obj);
+             goto end;
+          }
+        /* Fall through: Cocoa menu alloc failed, use in-canvas widget */
+     }
+#endif
+
 #ifdef HAVE_ELEMENTARY_X
    if (!_elm_config->disable_external_menu && sd->x.xwin) use_dbus = EINA_TRUE;
 #endif
diff --git a/src/lib/elementary/elm_cocoa_menu.c b/src/lib/elementary/elm_cocoa_menu.c
new file mode 100644
index 0000000000..92de78f029
--- /dev/null
+++ b/src/lib/elementary/elm_cocoa_menu.c
@@ -0,0 +1,338 @@
+/* elm_cocoa_menu.c
+ * Elementary bridge: elm_menu items <-> ecore_cocoa NSMenu.
+ * Pure C — no ObjC.
+ */
+#ifdef HAVE_CONFIG_H
+# include "elementary_config.h"
+#endif
+
+#ifdef HAVE_ELEMENTARY_COCOA
+
+#include <Elementary.h>
+#include <stdint.h>
+#include "elm_priv.h"
+#include "elm_widget_menu.h"
+
+struct _Elm_Cocoa_Menu
+{
+   Eo               *menu;       /* the elm_menu Eo object */
+   Ecore_Cocoa_Menu *root;       /* top-level NSMenu handle */
+   Eina_Hash        *elements;   /* int32 id -> Elm_Menu_Item_Data* */
+   Eina_Hash        *submenus;   /* int32 id -> Ecore_Cocoa_Menu*   */
+   unsigned          next_id;    /* monotonic counter, initial value 0, first id is 1 */
+};
+
+static void
+_submenu_hash_free_cb(void *data)
+{
+   ecore_cocoa_menu_free((Ecore_Cocoa_Menu *)data);
+}
+
+/* Forward declared in elm_menu.c Task 3.1 */
+void _elm_menu_item_activate(Elm_Object_Item *eo_item);
+
+/* ------------------------------------------------------------------ */
+/* Click callback (fires from NSMenuItem action via ecore_cocoa layer) */
+/* ------------------------------------------------------------------ */
+static void
+_item_click_cb(void *data, Ecore_Cocoa_Menu *menu EINA_UNUSED, int idx EINA_UNUSED)
+{
+   Elm_Object_Item *eo_item = data;
+   _elm_menu_item_activate(eo_item);
+}
+
+/* ------------------------------------------------------------------ */
+/* Internal: recursively add item and its submenu children             */
+/* ------------------------------------------------------------------ */
+static int
+_item_add_recursive(Elm_Cocoa_Menu *cm, Ecore_Cocoa_Menu *ns_parent,
+                    Elm_Menu_Item_Data *item)
+{
+   int id;
+   Eina_List *l;
+   Elm_Object_Item *eo_sub;
+
+   id = (int)++cm->next_id;
+
+   if (item->separator)
+     {
+        ecore_cocoa_menu_item_add_separator(ns_parent);
+        /* Separators are not tracked in the hash — no cocoa_idx assignment needed */
+     }
+   else if (item->submenu.items)
+     {
+        /* Top-level or submenu-bearing item: add placeholder, then recurse */
+        int ns_idx;
+        Ecore_Cocoa_Menu *sub_ns;
+
+        sub_ns = ecore_cocoa_menu_new(item->label ? item->label : "");
+        if (!sub_ns) return -1;
+
+        ns_idx = ecore_cocoa_menu_item_add(ns_parent,
+                                           item->label ? item->label : "",
+                                           NULL, NULL, NULL);
+        if (ns_idx < 0) { ecore_cocoa_menu_free(sub_ns); return -1; }
+
+        /* Assign the logical ID as NSMenuItem tag for stable lookup */
+        ecore_cocoa_menu_item_tag_set(ns_parent, ns_idx, id);
+        ecore_cocoa_menu_submenu_set(ns_parent, ns_idx, sub_ns);
+
+        if (!eina_hash_add(cm->elements, &id, item))
+          {
+             ecore_cocoa_menu_item_del(ns_parent, ns_idx);
+             ecore_cocoa_menu_free(sub_ns);
+             return -1;
+          }
+        if (!eina_hash_add(cm->submenus, &id, sub_ns))
+          {
+             eina_hash_del_by_key(cm->elements, &id);
+             ecore_cocoa_menu_item_del(ns_parent, ns_idx);
+             ecore_cocoa_menu_free(sub_ns);
+             return -1;
+          }
+        item->cocoa_idx  = id;
+        item->cocoa_menu = cm;
+
+        EINA_LIST_FOREACH(item->submenu.items, l, eo_sub)
+          {
+             ELM_MENU_ITEM_DATA_GET(eo_sub, subitem);
+             if (_item_add_recursive(cm, sub_ns, subitem) < 0)
+               {
+                  /* Parent registered OK; child failed. Log and continue
+                   * with partial menu rather than tearing down the parent. */
+                  WRN("_item_add_recursive: child add failed, partial menu");
+                  break;
+               }
+          }
+     }
+   else
+     {
+        /* Leaf item */
+        int ns_idx;
+
+        ns_idx = ecore_cocoa_menu_item_add(ns_parent,
+                                           item->label ? item->label : "",
+                                           NULL,
+                                           _item_click_cb,
+                                           EO_OBJ(item));
+        if (ns_idx < 0) return -1;
+
+        /* Assign the logical ID as NSMenuItem tag for stable lookup */
+        ecore_cocoa_menu_item_tag_set(ns_parent, ns_idx, id);
+
+        if (!eina_hash_add(cm->elements, &id, item)) return -1;
+        item->cocoa_idx  = id;
+        item->cocoa_menu = cm;
+     }
+
+   return id;
+}
+
+/* ------------------------------------------------------------------ */
+/* Public internal API                                                  */
+/* ------------------------------------------------------------------ */
+
+Elm_Cocoa_Menu *
+_elm_cocoa_menu_register(Eo *obj)
+{
+   Elm_Cocoa_Menu *cm;
+   Eina_List *l;
+   Elm_Object_Item *eo_item;
+
+   ELM_MENU_DATA_GET_OR_RETURN_VAL(obj, sd, NULL);
+
+   if (sd->cocoa_menu) return sd->cocoa_menu; /* already registered */
+
+   cm = calloc(1, sizeof(*cm));
+   if (EINA_UNLIKELY(!cm)) return NULL;
+
+   cm->menu = obj;
+   cm->next_id = 0;
+
+   cm->elements = eina_hash_int32_new(NULL);
+   if (!cm->elements) { free(cm); return NULL; }
+
+   cm->submenus = eina_hash_int32_new(_submenu_hash_free_cb);
+   if (!cm->submenus) { eina_hash_free(cm->elements); free(cm); return NULL; }
+
+   cm->root = ecore_cocoa_menu_new("MainMenu");
+   if (!cm->root)
+     {
+        eina_hash_free(cm->submenus);
+        eina_hash_free(cm->elements);
+        free(cm);
+        return NULL;
+     }
+
+   /* Set sd->cocoa_menu before the walk so that any re-entrant calls
+    * from EO callbacks during the walk will find the bridge in place. */
+   sd->cocoa_menu = cm;
+
+   /* Walk existing items (app may add items before or after register) */
+   EINA_LIST_FOREACH(sd->items, l, eo_item)
+     {
+        int ret;
+
+        ELM_MENU_ITEM_DATA_GET(eo_item, item);
+        ret = _item_add_recursive(cm, cm->root, item);
+        if (ret < 0)
+          WRN("Failed to add menu item '%s' to Cocoa menu — skipping",
+              item->label ? item->label : "(null)");
+     }
+
+   ecore_cocoa_menu_main_set(cm->root);
+
+   return cm;
+}
+
+void
+_elm_cocoa_menu_unregister(Eo *obj)
+{
+   ELM_MENU_DATA_GET_OR_RETURN(obj, sd);
+
+   if (!sd->cocoa_menu) return;
+
+   /* submenus hash has a free callback that calls ecore_cocoa_menu_free()
+    * on each value — free it before the root to avoid double-free since
+    * Cocoa also releases child NSMenus when the parent is released. */
+   eina_hash_free(sd->cocoa_menu->submenus);
+   ecore_cocoa_menu_free(sd->cocoa_menu->root);
+   eina_hash_free(sd->cocoa_menu->elements);
+   free(sd->cocoa_menu);
+   sd->cocoa_menu = NULL;
+}
+
+/* Promote a leaf NSMenuItem into a submenu-bearing item.
+ * Called when a child is added to an item that was originally created
+ * as a leaf (no children at creation time). */
+static Ecore_Cocoa_Menu *
+_promote_to_submenu(Elm_Cocoa_Menu *cm, Elm_Menu_Item_Data *parent_item)
+{
+   Ecore_Cocoa_Menu *parent_ns;
+   Ecore_Cocoa_Menu *sub_ns;
+   int               ns_idx;
+   int               parent_tag;
+
+   parent_tag = parent_item->cocoa_idx;
+
+   /* Find which NSMenu contains the parent item */
+   if (parent_item->parent && parent_item->parent->cocoa_idx > 0)
+     {
+        parent_ns = eina_hash_find(cm->submenus, &parent_item->parent->cocoa_idx);
+        if (!parent_ns) parent_ns = cm->root;
+     }
+   else
+     parent_ns = cm->root;
+
+   /* Create a new NSMenu for the children */
+   sub_ns = ecore_cocoa_menu_new(parent_item->label ? parent_item->label : "");
+   if (!sub_ns) return NULL;
+
+   /* Attach the submenu to the existing NSMenuItem identified by tag */
+   ns_idx = ecore_cocoa_menu_item_index_by_tag(parent_ns, parent_tag);
+   if (ns_idx < 0)
+     {
+        ecore_cocoa_menu_free(sub_ns);
+        return NULL;
+     }
+   ecore_cocoa_menu_submenu_set(parent_ns, ns_idx, sub_ns);
+
+   /* Track the submenu in our hash */
+   if (!eina_hash_add(cm->submenus, &parent_tag, sub_ns))
+     {
+        /* sub_ns is attached to the native menu via setSubmenu — Cocoa retains
+         * it, so ecore_cocoa_menu_free just drops our reference. */
+        ecore_cocoa_menu_free(sub_ns);
+        return NULL;
+     }
+
+   return sub_ns;
+}
+
+int
+_elm_cocoa_menu_item_add(Elm_Cocoa_Menu *cm, Elm_Object_Item *eo_item)
+{
+   Ecore_Cocoa_Menu *ns_parent;
+   int id;
+
+   if (!cm || !eo_item) return -1;
+
+   ELM_MENU_ITEM_DATA_GET(eo_item, item);
+
+   /* Determine which NSMenu is the parent */
+   if (item->parent && item->parent->cocoa_idx > 0)
+     {
+        ns_parent = eina_hash_find(cm->submenus, &item->parent->cocoa_idx);
+        if (!ns_parent)
+          {
+             /* Parent was created as a leaf — promote it to a submenu */
+             ns_parent = _promote_to_submenu(cm, item->parent);
+             if (!ns_parent) ns_parent = cm->root;
+          }
+     }
+   else
+     ns_parent = cm->root;
+
+   id = _item_add_recursive(cm, ns_parent, item);
+   return id;
+}
+
+void
+_elm_cocoa_menu_item_delete(Elm_Cocoa_Menu *cm, int id)
+{
+   Elm_Menu_Item_Data *item;
+   Ecore_Cocoa_Menu   *ns_menu;
+   Ecore_Cocoa_Menu   *sub;
+
+   if (!cm || id <= 0) return;
+
+   item = eina_hash_find(cm->elements, &id);
+   if (!item) return;
+
+   /* Determine which NSMenu contains this item */
+   if (item->parent && item->parent->cocoa_idx > 0)
+     {
+        ns_menu = eina_hash_find(cm->submenus, &item->parent->cocoa_idx);
+        if (!ns_menu) ns_menu = cm->root;
+     }
+   else
+     ns_menu = cm->root;
+
+   /* Use tag-based deletion — cocoa_idx is a logical ID stored as NSMenuItem.tag,
+    * not a positional index (which drifts after deletions). */
+   ecore_cocoa_menu_item_del_by_tag(ns_menu, item->cocoa_idx);
+   eina_hash_del_by_key(cm->elements, &id);
+
+   /* If it had a submenu, remove from hash (free callback handles ecore_cocoa_menu_free) */
+   sub = eina_hash_find(cm->submenus, &id);
+   if (sub)
+     eina_hash_del_by_key(cm->submenus, &id);
+}
+
+void
+_elm_cocoa_menu_update(Elm_Cocoa_Menu *cm)
+{
+   Eina_Iterator *it;
+   Elm_Menu_Item_Data *item;
+
+   if (!cm) return;
+
+   /* Sync enabled state for all tracked items */
+   it = eina_hash_iterator_data_new(cm->elements);
+   EINA_ITERATOR_FOREACH(it, item)
+     {
+        Ecore_Cocoa_Menu *ns_menu = cm->root;
+        if (item->parent && item->parent->cocoa_idx > 0)
+          {
+             Ecore_Cocoa_Menu *sub = eina_hash_find(cm->submenus,
+                                                    &item->parent->cocoa_idx);
+             if (sub) ns_menu = sub;
+          }
+        /* Use tag-based enabled_set — cocoa_idx is a logical tag, not a position */
+        ecore_cocoa_menu_item_enabled_set_by_tag(ns_menu, item->cocoa_idx,
+                                                 !elm_object_item_disabled_get(EO_OBJ(item)));
+     }
+   eina_iterator_free(it);
+}
+
+#endif /* HAVE_ELEMENTARY_COCOA */
diff --git a/src/lib/elementary/elm_menu.c b/src/lib/elementary/elm_menu.c
index 5914a82eff..4be1a0a34b 100644
--- a/src/lib/elementary/elm_menu.c
+++ b/src/lib/elementary/elm_menu.c
@@ -99,6 +99,9 @@ _elm_menu_item_elm_widget_item_disable(Eo *eo_item, Elm_Menu_Item_Data *item)
      elm_layout_signal_emit(VIEW(item), "elm,state,enabled", "elm");
 
    if (item->dbus_menu) _elm_dbus_menu_update(item->dbus_menu);
+#ifdef HAVE_ELEMENTARY_COCOA
+   if (item->cocoa_menu) _elm_cocoa_menu_update(item->cocoa_menu);
+#endif
    edje_object_message_signal_process(elm_layout_edje_get(VIEW(item)));
 }
 
@@ -474,6 +477,15 @@ _elm_dbus_menu_item_select_cb(Elm_Object_Item *obj_item)
   if (item->func) item->func((void *)(WIDGET_ITEM_DATA_GET(EO_OBJ(item))), WIDGET(item), obj_item);
 }
 
+/* Non-static forwarder used by elm_cocoa_menu.c to fire item callbacks */
+void
+_elm_menu_item_activate(Elm_Object_Item *eo_item)
+{
+   ELM_MENU_ITEM_DATA_GET(eo_item, item);
+   if (item->func)
+     item->func((void *)WIDGET_ITEM_DATA_GET(eo_item), WIDGET(item), eo_item);
+}
+
 static void
 _menu_item_select_cb(void *data,
                      Evas_Object *obj EINA_UNUSED,
@@ -732,6 +744,9 @@ _elm_menu_efl_canvas_group_group_del(Eo *obj, Elm_Menu_Data *sd)
    Elm_Object_Item *eo_item;
 
    _elm_dbus_menu_unregister(obj);
+#ifdef HAVE_ELEMENTARY_COCOA
+   _elm_cocoa_menu_unregister(obj);
+#endif
 
    if (sd->parent)
      {
@@ -1040,6 +1055,10 @@ _elm_menu_item_efl_object_destructor(Eo *eo_item, Elm_Menu_Item_Data *item)
 
    if (sd->dbus_menu)
      _elm_dbus_menu_item_delete(sd->dbus_menu, item->dbus_idx);
+#ifdef HAVE_ELEMENTARY_COCOA
+   if (sd->cocoa_menu)
+     _elm_cocoa_menu_item_delete(sd->cocoa_menu, item->cocoa_idx);
+#endif
 
    efl_destructor(efl_super(eo_item, ELM_MENU_ITEM_CLASS));
 }
@@ -1094,6 +1113,13 @@ _elm_menu_item_add(Eo *obj, Elm_Menu_Data *sd, Elm_Object_Item *parent, const ch
      it->dbus_idx = _elm_dbus_menu_item_add(sd->dbus_menu, eo_item);
      it->dbus_menu = sd->dbus_menu;
    }
+#ifdef HAVE_ELEMENTARY_COCOA
+   if (sd->cocoa_menu)
+     {
+       it->cocoa_idx  = _elm_cocoa_menu_item_add(sd->cocoa_menu, eo_item);
+       it->cocoa_menu = sd->cocoa_menu;
+     }
+#endif
    if (_elm_config->atspi_mode)
      {
         efl_access_added(eo_item);
@@ -1169,6 +1195,13 @@ _elm_menu_item_separator_add(Eo *obj, Elm_Menu_Data *sd, Elm_Object_Item *eo_p_i
    if (sd->dbus_menu)
      subitem->dbus_idx = _elm_dbus_menu_item_add(sd->dbus_menu,
                                                  eo_subitem);
+#ifdef HAVE_ELEMENTARY_COCOA
+   if (sd->cocoa_menu)
+     {
+       subitem->cocoa_idx  = _elm_cocoa_menu_item_add(sd->cocoa_menu, eo_subitem);
+       subitem->cocoa_menu = sd->cocoa_menu;
+     }
+#endif
    return eo_subitem;
 }
 
diff --git a/src/lib/elementary/elm_priv.h b/src/lib/elementary/elm_priv.h
index 1c72c26c40..4b62842a7f 100644
--- a/src/lib/elementary/elm_priv.h
+++ b/src/lib/elementary/elm_priv.h
@@ -833,6 +833,22 @@ void                 _elm_dbus_menu_app_menu_register(Ecore_X_Window xid, Eo *ob
 void                 _elm_dbus_menu_app_menu_unregister(Eo *obj);
 void                 _elm_dbus_menu_item_select_cb(Elm_Object_Item *obj_item);
 
+#ifdef HAVE_ELEMENTARY_COCOA
+/* Forward declaration — guarded to avoid duplicate typedef when
+ * elm_widget_menu.h is included in the same translation unit. */
+#ifndef _ELM_COCOA_MENU_TYPEDEF
+#define _ELM_COCOA_MENU_TYPEDEF
+typedef struct _Elm_Cocoa_Menu Elm_Cocoa_Menu;
+#endif
+Elm_Cocoa_Menu *_elm_cocoa_menu_register(Eo *obj);
+void            _elm_cocoa_menu_unregister(Eo *obj);
+int             _elm_cocoa_menu_item_add(Elm_Cocoa_Menu *cocoa_menu,
+                                         Elm_Object_Item *eo_item);
+void            _elm_cocoa_menu_item_delete(Elm_Cocoa_Menu *cocoa_menu,
+                                             int id);
+void            _elm_cocoa_menu_update(Elm_Cocoa_Menu *cocoa_menu);
+#endif /* HAVE_ELEMENTARY_COCOA */
+
 void                 _elm_menu_menu_bar_set(Eo *obj, Eina_Bool menu_bar);
 void                 _elm_menu_menu_bar_hide(Eo *obj);
 
diff --git a/src/lib/elementary/elm_widget_menu.h b/src/lib/elementary/elm_widget_menu.h
index dfcc538e90..97f1e648a5 100644
--- a/src/lib/elementary/elm_widget_menu.h
+++ b/src/lib/elementary/elm_widget_menu.h
@@ -7,6 +7,14 @@
 #include "elm_menu_item_eo.h"
 #include "elm_hover_eo.h"
 
+#ifdef HAVE_ELEMENTARY_COCOA
+/* Forward declaration only — elm_cocoa_menu.c owns the full definition */
+#ifndef _ELM_COCOA_MENU_TYPEDEF
+#define _ELM_COCOA_MENU_TYPEDEF
+typedef struct _Elm_Cocoa_Menu Elm_Cocoa_Menu;
+#endif
+#endif
+
 /* DO NOT USE THIS HEADER UNLESS YOU ARE PREPARED FOR BREAKING OF YOUR
  * CODE. THIS IS ELEMENTARY'S INTERNAL WIDGET API (for now) AND IS NOT
  * FINAL. CALL elm_widget_api_check(ELM_INTERNAL_API_VERSION) TO CHECK
@@ -36,6 +44,10 @@ struct _Elm_Menu_Data
    Evas_Coord            xloc, yloc;
    Elm_DBus_Menu        *dbus_menu;
 
+#ifdef HAVE_ELEMENTARY_COCOA
+   Elm_Cocoa_Menu       *cocoa_menu;
+#endif
+
    Eina_Bool             menu_bar : 1;
 };
 
@@ -52,6 +64,10 @@ struct _Elm_Menu_Item_Data
    unsigned int   idx;
    Elm_DBus_Menu *dbus_menu;
    int            dbus_idx;
+#ifdef HAVE_ELEMENTARY_COCOA
+   Elm_Cocoa_Menu *cocoa_menu;
+   int             cocoa_idx;
+#endif
 
    struct
    {
diff --git a/src/lib/elementary/meson.build b/src/lib/elementary/meson.build
index 1f07a668f3..0cf8436191 100644
--- a/src/lib/elementary/meson.build
+++ b/src/lib/elementary/meson.build
@@ -1005,6 +1005,7 @@ endif
 if get_option('cocoa')
   config_h.set('HAVE_ELEMENTARY_COCOA', '1')
   elementary_deps += ecore_cocoa
+  elementary_src += files('elm_cocoa_menu.c')
 endif
 
 if sys_windows == true

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to