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.