This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch fix-modern-macos
in repository efl.
View the commit online.
commit 95db05515ea8ffd2072f48deb903b7a77ca5c2b9
Author: Cedric BAIL <[email protected]>
AuthorDate: Wed Apr 29 17:59:25 2026 -0600
feat(elementary): auto-populate default macOS menu bar
When elm_win_main_menu_get is called on a Cocoa build, EFL now
pre-populates two default submenus:
<AppName>
Quit <AppName> Cmd+Q
File
New Window Cmd+N
The File / New Window entry is only present when an app has registered
a new-window callback via elm_app_new_window_cb_set. Registering or
clearing the callback after the menu is built adds or removes the File
menu live (reactive update via _elm_app_new_window_cb_changed).
Apps that want no default menu can opt out with
elm_app_default_menu_disable() before creating their first window.
Also adds _elm_cocoa_app_init() — a one-shot setup function that
installs the Cocoa Dock-reopen handler. Wiring lands in efl_ui_win.c
in the next commit.
---
po/POTFILES.in | 1 +
src/lib/ecore_cocoa/Ecore_Cocoa.h | 21 ++++
src/lib/ecore_cocoa/ecore_cocoa_menu.m | 21 ++++
src/lib/elementary/elm_cocoa_menu.c | 197 ++++++++++++++++++++++++++++++++-
src/lib/elementary/elm_main.c | 9 +-
src/lib/elementary/elm_priv.h | 3 +
6 files changed, 246 insertions(+), 6 deletions(-)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 00f11e6b6e..14975d426c 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1854,6 +1854,7 @@ src/lib/elementary/elm_calendar_item_eo.c
src/lib/elementary/elm_calendar_item_eo.h
src/lib/elementary/elm_calendar_item_eo.legacy.h
src/lib/elementary/elm_calendar_legacy.h
+src/lib/elementary/elm_cocoa_menu.c
src/lib/elementary/elm_check.h
src/lib/elementary/elm_check_legacy.h
src/lib/elementary/elm_clock.c
diff --git a/src/lib/ecore_cocoa/Ecore_Cocoa.h b/src/lib/ecore_cocoa/Ecore_Cocoa.h
index c51d0b37e1..782262e71e 100644
--- a/src/lib/ecore_cocoa/Ecore_Cocoa.h
+++ b/src/lib/ecore_cocoa/Ecore_Cocoa.h
@@ -739,12 +739,33 @@ EAPI void ecore_cocoa_menu_item_label_set(Ecore_Cocoa_Menu *menu,
* @param key Single-character UTF-8 string, or NULL/"" to clear.
*
* @since 1.28
+ * @see ecore_cocoa_menu_item_key_equivalent_set_by_tag
*/
EAPI void ecore_cocoa_menu_item_key_equivalent_set(Ecore_Cocoa_Menu *menu,
int idx,
const char *key)
EINA_ARG_NONNULL(1);
+/**
+ * Sets the keyboard equivalent for an existing menu item, identified by
+ * its NSMenuItem tag (the value set via ecore_cocoa_menu_item_tag_set()).
+ *
+ * Tag-based variant of ecore_cocoa_menu_item_key_equivalent_set; preferred
+ * over the positional form because tags survive item deletion while
+ * positional indices shift.
+ *
+ * @param menu The menu.
+ * @param tag Tag value previously assigned to the item via
+ * ecore_cocoa_menu_item_tag_set().
+ * @param key Single-character UTF-8 string, or NULL/"" to clear.
+ *
+ * @since 1.28
+ */
+EAPI void ecore_cocoa_menu_item_key_equivalent_set_by_tag(Ecore_Cocoa_Menu *menu,
+ int tag,
+ const char *key)
+ EINA_ARG_NONNULL(1);
+
/**
* Attaches a sub-menu to an existing menu item.
* @param menu The parent menu.
diff --git a/src/lib/ecore_cocoa/ecore_cocoa_menu.m b/src/lib/ecore_cocoa/ecore_cocoa_menu.m
index 004e7c6aa9..ba7c21d024 100644
--- a/src/lib/ecore_cocoa/ecore_cocoa_menu.m
+++ b/src/lib/ecore_cocoa/ecore_cocoa_menu.m
@@ -301,6 +301,27 @@ ecore_cocoa_menu_item_key_equivalent_set(Ecore_Cocoa_Menu *menu,
[item setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
}
+EAPI void
+ecore_cocoa_menu_item_key_equivalent_set_by_tag(Ecore_Cocoa_Menu *menu,
+ int tag,
+ const char *key)
+{
+ NSMenuItem *item;
+
+ EINA_SAFETY_ON_NULL_RETURN(menu);
+
+ item = [menu->ns_menu itemWithTag:(NSInteger)tag];
+ if (!item) return;
+
+ if (!key || !key[0])
+ {
+ [item setKeyEquivalent:@""];
+ return;
+ }
+ [item setKeyEquivalent:[NSString stringWithUTF8String:key]];
+ [item setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
+}
+
EAPI void
ecore_cocoa_menu_submenu_set(Ecore_Cocoa_Menu *menu, int idx,
Ecore_Cocoa_Menu *submenu)
diff --git a/src/lib/elementary/elm_cocoa_menu.c b/src/lib/elementary/elm_cocoa_menu.c
index 92de78f029..8556633b40 100644
--- a/src/lib/elementary/elm_cocoa_menu.c
+++ b/src/lib/elementary/elm_cocoa_menu.c
@@ -22,6 +22,18 @@ struct _Elm_Cocoa_Menu
unsigned next_id; /* monotonic counter, initial value 0, first id is 1 */
};
+/* References to the auto-populated default menu submenus. NULL if the
+ * default menu wasn't populated (because elm_app_default_menu_disable()
+ * was called) or if the corresponding menu item hasn't been added yet. */
+static Elm_Object_Item *_default_app_item = NULL;
+static Elm_Object_Item *_default_quit_item = NULL;
+static Elm_Object_Item *_default_file_item = NULL;
+static Elm_Object_Item *_default_new_window_item = NULL;
+static Eina_Bool _default_menu_active = EINA_FALSE;
+
+/* Tracks whether _elm_cocoa_app_init has installed the reopen handler. */
+static Eina_Bool _cocoa_app_initialized = EINA_FALSE;
+
static void
_submenu_hash_free_cb(void *data)
{
@@ -128,6 +140,108 @@ _item_add_recursive(Elm_Cocoa_Menu *cm, Ecore_Cocoa_Menu *ns_parent,
return id;
}
+/* ------------------------------------------------------------------ */
+/* Default-menu callbacks */
+/* ------------------------------------------------------------------ */
+
+static void
+_cb_default_quit(void *data EINA_UNUSED, Evas_Object *obj EINA_UNUSED,
+ void *event_info EINA_UNUSED)
+{
+ elm_exit();
+}
+
+static void
+_cb_default_new_window(void *data EINA_UNUSED, Evas_Object *obj EINA_UNUSED,
+ void *event_info EINA_UNUSED)
+{
+ elm_app_new_window_request();
+}
+
+static const char *
+_resolve_app_name(void)
+{
+ const char *n = elm_app_name_get();
+ if (n && n[0]) return n;
+ /* Fallback: a generic but stable string. Cocoa-side CFBundleName
+ * lookup is left for a future refinement; CFBundleName is normally
+ * already wired into elm_app_name via the launcher. */
+ return E_("Application");
+}
+
+static void
+_set_key_equiv(Elm_Object_Item *eo_item, const char *key)
+{
+ Ecore_Cocoa_Menu *ns_menu;
+ Ecore_Cocoa_Menu *sub;
+
+ /* Reach into the menu bridge to set the NSMenuItem key equivalent.
+ * eo_item -> item->cocoa_menu / item->cocoa_idx is set when the item
+ * was added by _item_add_recursive. */
+ ELM_MENU_ITEM_DATA_GET(eo_item, item);
+ if (!item || !item->cocoa_menu) return;
+
+ /* Find the NSMenu that directly contains this item */
+ ns_menu = item->cocoa_menu->root;
+ if (item->parent && item->parent->cocoa_idx > 0)
+ {
+ sub = eina_hash_find(item->cocoa_menu->submenus,
+ &item->parent->cocoa_idx);
+ if (sub) ns_menu = sub;
+ }
+ ecore_cocoa_menu_item_key_equivalent_set_by_tag(ns_menu, item->cocoa_idx, key);
+}
+
+static void
+_elm_cocoa_menu_populate_defaults(Eo *menu)
+{
+ const char *app_name;
+ char buf[256];
+ Elm_App_New_Window_Cb cb;
+
+ if (_elm_app_default_menu_disabled_get()) return;
+
+ app_name = _resolve_app_name();
+
+ /* <AppName> menu — always present */
+ _default_app_item = elm_menu_item_add(menu, NULL, NULL, app_name,
+ NULL, NULL);
+ if (!_default_app_item) return;
+
+ /* Mark active only after first successful item add. This guards the
+ * multi-window idempotency check: a second register on a different
+ * window finds _default_menu_active=TRUE and skips populating defaults
+ * a second time. */
+ _default_menu_active = EINA_TRUE;
+
+ /* Concatenate translated verb + app_name separately to avoid
+ * format-string injection via a malicious PO file
+ * (e.g., "Quit %s" → "Beenden %n"). */
+ snprintf(buf, sizeof(buf), "%s %s", E_("Quit"), app_name);
+ _default_quit_item = elm_menu_item_add(menu, _default_app_item, NULL, buf,
+ _cb_default_quit, NULL);
+ if (_default_quit_item)
+ _set_key_equiv(_default_quit_item, "q");
+
+ /* File menu — only if a new-window cb is registered */
+ cb = NULL;
+ elm_app_new_window_cb_get(&cb, NULL);
+ if (cb)
+ {
+ _default_file_item = elm_menu_item_add(menu, NULL, NULL,
+ E_("File"), NULL, NULL);
+ if (_default_file_item)
+ {
+ _default_new_window_item =
+ elm_menu_item_add(menu, _default_file_item, NULL,
+ E_("New Window"),
+ _cb_default_new_window, NULL);
+ if (_default_new_window_item)
+ _set_key_equiv(_default_new_window_item, "n");
+ }
+ }
+}
+
/* ------------------------------------------------------------------ */
/* Public internal API */
/* ------------------------------------------------------------------ */
@@ -138,6 +252,7 @@ _elm_cocoa_menu_register(Eo *obj)
Elm_Cocoa_Menu *cm;
Eina_List *l;
Elm_Object_Item *eo_item;
+ Eina_Bool was_first;
ELM_MENU_DATA_GET_OR_RETURN_VAL(obj, sd, NULL);
@@ -180,7 +295,20 @@ _elm_cocoa_menu_register(Eo *obj)
item->label ? item->label : "(null)");
}
- ecore_cocoa_menu_main_set(cm->root);
+ /* Track whether this is the first registration (before populate, so
+ * we can decide whether to install as main menu). */
+ was_first = !_default_menu_active;
+
+ /* Populate the platform-default menu (App + File submenus) */
+ _elm_cocoa_menu_populate_defaults(obj);
+
+ /* Only install as main menu on first registration. On second+
+ * window, the app-level menu is already set — replacing it with
+ * this window's empty root would blank the macOS menu bar.
+ * The disabled-defaults branch installs unconditionally because the
+ * app fully controls its menu and legitimately needs install. */
+ if (was_first || _elm_app_default_menu_disabled_get())
+ ecore_cocoa_menu_main_set(cm->root);
return cm;
}
@@ -200,6 +328,15 @@ _elm_cocoa_menu_unregister(Eo *obj)
eina_hash_free(sd->cocoa_menu->elements);
free(sd->cocoa_menu);
sd->cocoa_menu = NULL;
+
+ /* Clear default-menu state so a future register starts fresh.
+ * Items are owned by the elm_menu widget; its destructor frees them.
+ * Do NOT call elm_object_item_del here — the widget is being torn down. */
+ _default_app_item = NULL;
+ _default_quit_item = NULL;
+ _default_file_item = NULL;
+ _default_new_window_item = NULL;
+ _default_menu_active = EINA_FALSE;
}
/* Promote a leaf NSMenuItem into a submenu-bearing item.
@@ -335,4 +472,62 @@ _elm_cocoa_menu_update(Elm_Cocoa_Menu *cm)
eina_iterator_free(it);
}
+/* Cocoa-side definition of the cb-changed hook.
+ * Adds or removes the File / New Window menu item live when the app
+ * registers or clears its new-window callback after the menu has
+ * already been built. */
+void
+_elm_app_new_window_cb_changed(Eina_Bool now_set)
+{
+ Eo *menu;
+
+ /* No effect if defaults were disabled or the menu hasn't been built
+ * yet (in which case _default_app_item is NULL). */
+ if (!_default_menu_active) return;
+ if (!_default_app_item) return;
+ /* No need to re-check elm_app_default_menu_disable() — _default_menu_active
+ * is set only after that check passed in _elm_cocoa_menu_populate_defaults,
+ * and the disable flag is write-once. */
+
+ /* The menu is the parent widget of _default_app_item */
+ menu = elm_object_item_widget_get(_default_app_item);
+ if (!menu) return;
+
+ if (now_set && !_default_file_item)
+ {
+ _default_file_item = elm_menu_item_add(menu, NULL, NULL,
+ E_("File"), NULL, NULL);
+ if (_default_file_item)
+ {
+ _default_new_window_item =
+ elm_menu_item_add(menu, _default_file_item, NULL,
+ E_("New Window"),
+ _cb_default_new_window, NULL);
+ if (_default_new_window_item)
+ _set_key_equiv(_default_new_window_item, "n");
+ }
+ }
+ else if (!now_set && _default_file_item)
+ {
+ /* Removing the parent removes its children too */
+ elm_object_item_del(_default_file_item);
+ _default_file_item = NULL;
+ _default_new_window_item = NULL;
+ }
+}
+
+static Eina_Bool
+_elm_cocoa_reopen_handler(Eina_Bool has_visible EINA_UNUSED)
+{
+ return elm_app_new_window_request();
+}
+
+void
+_elm_cocoa_app_init(void)
+{
+ if (_cocoa_app_initialized) return;
+ _cocoa_app_initialized = EINA_TRUE;
+ ecore_cocoa_reopen_cb_set(_elm_cocoa_reopen_handler);
+}
+
#endif /* HAVE_ELEMENTARY_COCOA */
diff --git a/src/lib/elementary/elm_main.c b/src/lib/elementary/elm_main.c
index 27823d3fef..ff5d32dd40 100644
--- a/src/lib/elementary/elm_main.c
+++ b/src/lib/elementary/elm_main.c
@@ -675,16 +675,15 @@ _elm_app_default_menu_disabled_get(void)
return _app_default_menu_disabled;
}
-/* Default no-op definition. The Cocoa menu bridge (elm_cocoa_menu.c)
- * provides a real implementation in Cocoa builds; until that lands in
- * a follow-up commit, this stub is the only definition and prevents
- * a link error. When the Cocoa implementation arrives this stub will
- * be removed (or guarded) so there is exactly one definition. */
+#ifndef HAVE_ELEMENTARY_COCOA
+/* Default no-op for non-Cocoa platforms. The Cocoa menu bridge in
+ * elm_cocoa_menu.c provides the real implementation. */
void
_elm_app_new_window_cb_changed(Eina_Bool now_set EINA_UNUSED)
{
/* No-op: only the Cocoa menu bridge cares about live cb changes. */
}
+#endif
static Eina_Bool _elm_need_e_dbus = EINA_FALSE;
static void *e_dbus_handle = NULL;
diff --git a/src/lib/elementary/elm_priv.h b/src/lib/elementary/elm_priv.h
index 6f85d60113..14c495257c 100644
--- a/src/lib/elementary/elm_priv.h
+++ b/src/lib/elementary/elm_priv.h
@@ -847,6 +847,9 @@ int _elm_cocoa_menu_item_add(Elm_Cocoa_Menu *cocoa_menu,
void _elm_cocoa_menu_item_delete(Elm_Cocoa_Menu *cocoa_menu,
int id);
void _elm_cocoa_menu_update(Elm_Cocoa_Menu *cocoa_menu);
+/* Initialises Cocoa-app-level wiring. Idempotent — safe to call
+ * multiple times. Currently sets up the macOS Dock-reopen handler. */
+void _elm_cocoa_app_init(void);
#endif /* HAVE_ELEMENTARY_COCOA */
void _elm_menu_menu_bar_set(Eo *obj, Eina_Bool menu_bar);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.