Browse Source

[macOS] Improve native menu open/close callbacks.

bruvzg 1 year ago
parent
commit
1f7bf27780

+ 10 - 1
doc/classes/NativeMenu.xml

@@ -473,6 +473,14 @@
 				[b]Note:[/b] This method is implemented on macOS and Windows.
 			</description>
 		</method>
+		<method name="is_opened" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="rid" type="RID" />
+			<description>
+				Returns [code]true[/code] if the menu is currently opened.
+				[b]Note:[/b] This method is implemented only on macOS.
+			</description>
+		</method>
 		<method name="is_system_menu" qualifiers="const">
 			<return type="bool" />
 			<param index="0" name="rid" type="RID" />
@@ -699,6 +707,7 @@
 			<param index="1" name="callback" type="Callable" />
 			<description>
 				Registers callable to emit when the menu is about to show.
+				[b]Note:[/b] The OS can simulate menu opening to track menu item changes and global shortcuts, in which case the corresponding close callback is not triggered. Use [method is_opened] to check if the menu is currently opened.
 				[b]Note:[/b] This method is implemented only on macOS.
 			</description>
 		</method>
@@ -707,7 +716,7 @@
 			<param index="0" name="rid" type="RID" />
 			<param index="1" name="callback" type="Callable" />
 			<description>
-				Registers callable to emit when the menu is about to closed.
+				Registers callable to emit after the menu is closed.
 				[b]Note:[/b] This method is implemented only on macOS.
 			</description>
 		</method>

+ 8 - 1
platform/macos/godot_menu_delegate.mm

@@ -40,13 +40,20 @@
 - (void)doNothing:(id)sender {
 }
 
-- (void)menuNeedsUpdate:(NSMenu *)menu {
+- (void)menuWillOpen:(NSMenu *)menu {
 	if (NativeMenu::get_singleton()) {
 		NativeMenuMacOS *nmenu = (NativeMenuMacOS *)NativeMenu::get_singleton();
 		nmenu->_menu_open(menu);
 	}
 }
 
+- (void)menuNeedsUpdate:(NSMenu *)menu {
+	if (NativeMenu::get_singleton()) {
+		NativeMenuMacOS *nmenu = (NativeMenuMacOS *)NativeMenu::get_singleton();
+		nmenu->_menu_need_update(menu);
+	}
+}
+
 - (void)menuDidClose:(NSMenu *)menu {
 	if (NativeMenu::get_singleton()) {
 		NativeMenuMacOS *nmenu = (NativeMenuMacOS *)NativeMenu::get_singleton();

+ 5 - 0
platform/macos/native_menu_macos.h

@@ -73,8 +73,11 @@ class NativeMenuMacOS : public NativeMenu {
 public:
 	void _register_system_menus(NSMenu *p_main_menu, NSMenu *p_application_menu, NSMenu *p_window_menu, NSMenu *p_help_menu, NSMenu *p_dock_menu);
 	NSMenu *_get_dock_menu();
+
+	void _menu_need_update(NSMenu *p_menu);
 	void _menu_open(NSMenu *p_menu);
 	void _menu_close(NSMenu *p_menu);
+	void _menu_close_cb(const RID &p_rid);
 
 	virtual bool has_feature(Feature p_feature) const override;
 
@@ -98,6 +101,8 @@ public:
 	virtual void set_minimum_width(const RID &p_rid, float p_width) override;
 	virtual float get_minimum_width(const RID &p_rid) const override;
 
+	virtual bool is_opened(const RID &p_rid) const override;
+
 	virtual int add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag = Variant(), int p_index = -1) override;
 	virtual int add_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override;
 	virtual int add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override;

+ 33 - 8
platform/macos/native_menu_macos.mm

@@ -91,11 +91,22 @@ void NativeMenuMacOS::_menu_open(NSMenu *p_menu) {
 	if (menu_lookup.has(p_menu)) {
 		MenuData *md = menus.get_or_null(menu_lookup[p_menu]);
 		if (md) {
+			// Note: Set "is_open" flag, but do not call callback, menu items can't be modified during this call and "_menu_need_update" will be called right before it.
 			md->is_open = true;
+		}
+	}
+}
+
+void NativeMenuMacOS::_menu_need_update(NSMenu *p_menu) {
+	if (menu_lookup.has(p_menu)) {
+		MenuData *md = menus.get_or_null(menu_lookup[p_menu]);
+		if (md) {
+			// Note: "is_open" flag is set by "_menu_open", this method is always called before menu is shown, but might be called for the other reasons as well.
 			if (md->open_cb.is_valid()) {
 				Variant ret;
 				Callable::CallError ce;
 
+				// Callback is called directly, since it's expected to modify menu items before it's shown.
 				md->open_cb.callp(nullptr, 0, ret, ce);
 				if (ce.error != Callable::CallError::CALL_OK) {
 					ERR_PRINT(vformat("Failed to execute menu open callback: %s.", Variant::get_callable_error_text(md->open_cb, nullptr, 0, ce)));
@@ -110,15 +121,22 @@ void NativeMenuMacOS::_menu_close(NSMenu *p_menu) {
 		MenuData *md = menus.get_or_null(menu_lookup[p_menu]);
 		if (md) {
 			md->is_open = false;
-			if (md->close_cb.is_valid()) {
-				Variant ret;
-				Callable::CallError ce;
 
-				md->close_cb.callp(nullptr, 0, ret, ce);
-				if (ce.error != Callable::CallError::CALL_OK) {
-					ERR_PRINT(vformat("Failed to execute menu close callback: %s.", Variant::get_callable_error_text(md->close_cb, nullptr, 0, ce)));
-				}
-			}
+			// Callback called deferred, since it should not modify menu items during "_menu_close" call.
+			callable_mp(this, &NativeMenuMacOS::_menu_close_cb).call_deferred(menu_lookup[p_menu]);
+		}
+	}
+}
+
+void NativeMenuMacOS::_menu_close_cb(const RID &p_rid) {
+	MenuData *md = menus.get_or_null(p_rid);
+	if (md->close_cb.is_valid()) {
+		Variant ret;
+		Callable::CallError ce;
+
+		md->close_cb.callp(nullptr, 0, ret, ce);
+		if (ce.error != Callable::CallError::CALL_OK) {
+			ERR_PRINT(vformat("Failed to execute menu close callback: %s.", Variant::get_callable_error_text(md->close_cb, nullptr, 0, ce)));
 		}
 	}
 }
@@ -328,6 +346,13 @@ float NativeMenuMacOS::get_minimum_width(const RID &p_rid) const {
 	return md->menu.minimumWidth * DisplayServer::get_singleton()->screen_get_max_scale();
 }
 
+bool NativeMenuMacOS::is_opened(const RID &p_rid) const {
+	const MenuData *md = menus.get_or_null(p_rid);
+	ERR_FAIL_NULL_V(md, false);
+
+	return md->is_open;
+}
+
 int NativeMenuMacOS::add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag, int p_index) {
 	MenuData *md = menus.get_or_null(p_rid);
 	MenuData *md_sub = menus.get_or_null(p_submenu_rid);

+ 5 - 0
platform/windows/native_menu_windows.cpp

@@ -231,6 +231,11 @@ float NativeMenuWindows::get_minimum_width(const RID &p_rid) const {
 	return 0.f;
 }
 
+bool NativeMenuWindows::is_opened(const RID &p_rid) const {
+	// Not supported.
+	return false;
+}
+
 int NativeMenuWindows::add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag, int p_index) {
 	MenuData *md = menus.get_or_null(p_rid);
 	MenuData *md_sub = menus.get_or_null(p_submenu_rid);

+ 2 - 0
platform/windows/native_menu_windows.h

@@ -90,6 +90,8 @@ public:
 	virtual void set_minimum_width(const RID &p_rid, float p_width) override;
 	virtual float get_minimum_width(const RID &p_rid) const override;
 
+	virtual bool is_opened(const RID &p_rid) const override;
+
 	virtual int add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag = Variant(), int p_index = -1) override;
 	virtual int add_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override;
 	virtual int add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override;

+ 7 - 0
servers/display/native_menu.cpp

@@ -56,6 +56,8 @@ void NativeMenu::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_minimum_width", "rid", "width"), &NativeMenu::set_minimum_width);
 	ClassDB::bind_method(D_METHOD("get_minimum_width", "rid"), &NativeMenu::get_minimum_width);
 
+	ClassDB::bind_method(D_METHOD("is_opened", "rid"), &NativeMenu::is_opened);
+
 	ClassDB::bind_method(D_METHOD("add_submenu_item", "rid", "label", "submenu_rid", "tag", "index"), &NativeMenu::add_submenu_item, DEFVAL(Variant()), DEFVAL(-1));
 	ClassDB::bind_method(D_METHOD("add_item", "rid", "label", "callback", "key_callback", "tag", "accelerator", "index"), &NativeMenu::add_item, DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(Variant()), DEFVAL(Key::NONE), DEFVAL(-1));
 	ClassDB::bind_method(D_METHOD("add_check_item", "rid", "label", "callback", "key_callback", "tag", "accelerator", "index"), &NativeMenu::add_check_item, DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(Variant()), DEFVAL(Key::NONE), DEFVAL(-1));
@@ -200,6 +202,11 @@ Callable NativeMenu::get_popup_close_callback(const RID &p_rid) const {
 	return Callable();
 }
 
+bool NativeMenu::is_opened(const RID &p_rid) const {
+	WARN_PRINT("Global menus are not supported on this platform.");
+	return false;
+}
+
 void NativeMenu::set_minimum_width(const RID &p_rid, float p_width) {
 	WARN_PRINT("Global menus are not supported on this platform.");
 }

+ 2 - 0
servers/display/native_menu.h

@@ -90,6 +90,8 @@ public:
 	virtual void set_minimum_width(const RID &p_rid, float p_width);
 	virtual float get_minimum_width(const RID &p_rid) const;
 
+	virtual bool is_opened(const RID &p_rid) const;
+
 	virtual int add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag = Variant(), int p_index = -1);
 	virtual int add_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1);
 	virtual int add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1);