diff options
35 files changed, 1888 insertions, 759 deletions
diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 7bbfd8077a..9ce2377611 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -119,7 +119,7 @@ <param index="6" name="callback" type="Callable" /> <description> Displays OS native dialog for selecting files or directories in the file system. - Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths[/code]. + Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths, int selected_filter_index[/code]. [b]Note:[/b] This method is implemented if the display server has the [code]FEATURE_NATIVE_DIALOG[/code] feature. [b]Note:[/b] This method is implemented on Linux, Windows and macOS. [b]Note:[/b] [param current_directory] might be ignored. @@ -577,6 +577,16 @@ [b]Note:[/b] This method is implemented only on macOS. </description> </method> + <method name="global_menu_is_item_hidden" qualifiers="const"> + <return type="bool" /> + <param index="0" name="menu_root" type="String" /> + <param index="1" name="idx" type="int" /> + <description> + Returns [code]true[/code] if the item at index [param idx] is hidden. + See [method global_menu_set_item_hidden] for more info on how to hide an item. + [b]Note:[/b] This method is implemented only on macOS. + </description> + </method> <method name="global_menu_is_item_radio_checkable" qualifiers="const"> <return type="bool" /> <param index="0" name="menu_root" type="String" /> @@ -648,6 +658,27 @@ [b]Note:[/b] This method is implemented only on macOS. </description> </method> + <method name="global_menu_set_item_hidden"> + <return type="void" /> + <param index="0" name="menu_root" type="String" /> + <param index="1" name="idx" type="int" /> + <param index="2" name="hidden" type="bool" /> + <description> + Hides/shows the item at index [param idx]. When it is hidden, an item does not appear in a menu and its action cannot be invoked. + [b]Note:[/b] This method is implemented only on macOS. + </description> + </method> + <method name="global_menu_set_item_hover_callbacks"> + <return type="void" /> + <param index="0" name="menu_root" type="String" /> + <param index="1" name="idx" type="int" /> + <param index="2" name="callback" type="Callable" /> + <description> + Sets the callback of the item at index [param idx]. The callback is emitted when an item is hovered. + [b]Note:[/b] The [param callback] Callable needs to accept exactly one Variant parameter, the parameter passed to the Callable will be the value passed to the [code]tag[/code] parameter when the menu item was created. + [b]Note:[/b] This method is implemented only on macOS. + </description> + </method> <method name="global_menu_set_item_icon"> <return type="void" /> <param index="0" name="menu_root" type="String" /> @@ -751,6 +782,15 @@ [b]Note:[/b] This method is implemented only on macOS. </description> </method> + <method name="global_menu_set_popup_callbacks"> + <return type="void" /> + <param index="0" name="menu_root" type="String" /> + <param index="1" name="open_callback" type="Callable" /> + <param index="2" name="close_callback" type="Callable" /> + <description> + Registers callables to emit when the menu is respectively about to show or closed. + </description> + </method> <method name="has_feature" qualifiers="const"> <return type="bool" /> <param index="0" name="feature" type="int" enum="DisplayServer.Feature" /> diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index 208253d617..31659d4d4e 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -835,35 +835,9 @@ ConnectDialog::~ConnectDialog() { ////////////////////////////////////////// -// Originally copied and adapted from EditorProperty, try to keep style in sync. Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const { - // `p_text` is expected to be something like this: - // - `class|Control||Control brief description.`; - // - `signal|gui_input|(event: InputEvent)|gui_input description.`; - // - `../../.. :: _on_gui_input()`. - // Note that the description can be empty or contain `|`. - PackedStringArray slices = p_text.split("|", true, 3); - if (slices.size() < 4) { - return nullptr; // Use default tooltip instead. - } - - String item_type = (slices[0] == "class") ? TTR("Class:") : TTR("Signal:"); - String item_name = slices[1].strip_edges(); - String item_params = slices[2].strip_edges(); - String item_descr = slices[3].strip_edges(); - - String text = item_type + " [u][b]" + item_name + "[/b][/u]" + item_params + "\n"; - if (item_descr.is_empty()) { - text += "[i]" + TTR("No description.") + "[/i]"; - } else { - text += item_descr; - } - - EditorHelpBit *help_bit = memnew(EditorHelpBit); - help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1)); - help_bit->set_text(text); - - return help_bit; + // If it's not a doc tooltip, fallback to the default one. + return p_text.contains("::") ? nullptr : memnew(EditorHelpTooltip(p_text)); } struct _ConnectionsDockMethodInfoSort { @@ -1341,7 +1315,6 @@ void ConnectionsDock::update_tree() { while (native_base != StringName()) { String class_name; String doc_class_name; - String class_brief; Ref<Texture2D> class_icon; List<MethodInfo> class_signals; @@ -1355,21 +1328,8 @@ void ConnectionsDock::update_tree() { if (doc_class_name.is_empty()) { doc_class_name = script_base->get_path().trim_prefix("res://").quote(); } - - // For a script class, the cache is filled each time. - if (!doc_class_name.is_empty()) { - if (descr_cache.has(doc_class_name)) { - descr_cache[doc_class_name].clear(); - } - HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name); - if (F) { - class_brief = F->value.brief_description; - for (int i = 0; i < F->value.signals.size(); i++) { - descr_cache[doc_class_name][F->value.signals[i].name] = F->value.signals[i].description; - } - } else { - doc_class_name = String(); - } + if (!doc_class_name.is_empty() && !doc_data->class_list.find(doc_class_name)) { + doc_class_name = String(); } class_icon = editor_data.get_script_icon(script_base); @@ -1398,18 +1358,9 @@ void ConnectionsDock::update_tree() { script_base = base; } else { class_name = native_base; - doc_class_name = class_name; - - HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name); - if (F) { - class_brief = DTR(F->value.brief_description); - // For a native class, the cache is filled once. - if (!descr_cache.has(doc_class_name)) { - for (int i = 0; i < F->value.signals.size(); i++) { - descr_cache[doc_class_name][F->value.signals[i].name] = DTR(F->value.signals[i].description); - } - } - } else { + doc_class_name = native_base; + + if (!doc_data->class_list.find(doc_class_name)) { doc_class_name = String(); } @@ -1434,8 +1385,8 @@ void ConnectionsDock::update_tree() { section_item = tree->create_item(root); section_item->set_text(0, class_name); - // `|` separators used in `make_custom_tooltip()` for formatting. - section_item->set_tooltip_text(0, "class|" + class_name + "||" + class_brief); + // `|` separators used in `EditorHelpTooltip` for formatting. + section_item->set_tooltip_text(0, "class|" + doc_class_name + "||"); section_item->set_icon(0, class_icon); section_item->set_selectable(0, false); section_item->set_editable(0, false); @@ -1466,22 +1417,8 @@ void ConnectionsDock::update_tree() { sinfo["args"] = argnames; signal_item->set_metadata(0, sinfo); signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal"))); - - // Set tooltip with the signal's documentation. - { - String descr; - - HashMap<StringName, HashMap<StringName, String>>::ConstIterator G = descr_cache.find(doc_class_name); - if (G) { - HashMap<StringName, String>::ConstIterator F = G->value.find(signal_name); - if (F) { - descr = F->value; - } - } - - // `|` separators used in `make_custom_tooltip()` for formatting. - signal_item->set_tooltip_text(0, "signal|" + String(signal_name) + "|" + signame.trim_prefix(mi.name) + "|" + descr); - } + // `|` separators used in `EditorHelpTooltip` for formatting. + signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name)); // List existing connections. List<Object::Connection> existing_connections; diff --git a/editor/connections_dialog.h b/editor/connections_dialog.h index b07b08ecc7..7316a770ec 100644 --- a/editor/connections_dialog.h +++ b/editor/connections_dialog.h @@ -231,8 +231,6 @@ class ConnectionsDock : public VBoxContainer { PopupMenu *slot_menu = nullptr; LineEdit *search_box = nullptr; - HashMap<StringName, HashMap<StringName, String>> descr_cache; - void _filter_changed(const String &p_text); void _make_or_edit_connection(); diff --git a/editor/create_dialog.cpp b/editor/create_dialog.cpp index 1c8226e5e1..0e025b4430 100644 --- a/editor/create_dialog.cpp +++ b/editor/create_dialog.cpp @@ -500,10 +500,11 @@ void CreateDialog::select_type(const String &p_type, bool p_center_on_item) { to_select->select(0); search_options->scroll_to_item(to_select, p_center_on_item); - if (EditorHelp::get_doc_data()->class_list.has(p_type) && !DTR(EditorHelp::get_doc_data()->class_list[p_type].brief_description).is_empty()) { + String text = help_bit->get_class_description(p_type); + if (!text.is_empty()) { // Display both class name and description, since the help bit may be displayed // far away from the location (especially if the dialog was resized to be taller). - help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, DTR(EditorHelp::get_doc_data()->class_list[p_type].brief_description))); + help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, text)); help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); } else { // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. diff --git a/editor/editor_build_profile.cpp b/editor/editor_build_profile.cpp index bca30b2d2d..c3087d797a 100644 --- a/editor/editor_build_profile.cpp +++ b/editor/editor_build_profile.cpp @@ -646,24 +646,21 @@ void EditorBuildProfileManager::_class_list_item_selected() { Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { - String class_name = md; - String class_description; - - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); - if (E) { - class_description = DTR(E->value.brief_description); + String text = description_bit->get_class_description(md); + if (!text.is_empty()) { + // Display both class name and description, since the help bit may be displayed + // far away from the location (especially if the dialog was resized to be taller). + description_bit->set_text(vformat("[b]%s[/b]: %s", md, text)); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); + } else { + // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. + description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); } - - description_bit->set_text(class_description); } else if (md.get_type() == Variant::INT) { - int build_option_id = md; - String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id)); - - description_bit->set_text(TTRGET(build_option_description)); - return; - } else { - return; + String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption((int)md)); + description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(build_option_description))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); } } diff --git a/editor/editor_feature_profile.cpp b/editor/editor_feature_profile.cpp index 50406bea6a..5a44ae1aba 100644 --- a/editor/editor_feature_profile.cpp +++ b/editor/editor_feature_profile.cpp @@ -555,21 +555,22 @@ void EditorFeatureProfileManager::_class_list_item_selected() { Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { - String class_name = md; - String class_description; - - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); - if (E) { - class_description = DTR(E->value.brief_description); + String text = description_bit->get_class_description(md); + if (!text.is_empty()) { + // Display both class name and description, since the help bit may be displayed + // far away from the location (especially if the dialog was resized to be taller). + description_bit->set_text(vformat("[b]%s[/b]: %s", md, text)); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); + } else { + // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. + description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); } - - description_bit->set_text(class_description); } else if (md.get_type() == Variant::INT) { - int feature_id = md; - String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature(feature_id)); + String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature((int)md)); + description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(feature_description))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - description_bit->set_text(TTRGET(feature_description)); return; } else { return; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index b3bcb9f014..588b657d0d 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -38,6 +38,7 @@ #include "doc_data_compressed.gen.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" +#include "editor/editor_property_name_processor.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" @@ -2587,7 +2588,7 @@ DocTools *EditorHelp::get_doc_data() { return doc; } -//// EditorHelpBit /// +/// EditorHelpBit /// void EditorHelpBit::_go_to_help(String p_what) { EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); @@ -2620,6 +2621,179 @@ void EditorHelpBit::_meta_clicked(String p_select) { } } +String EditorHelpBit::get_class_description(const StringName &p_class_name) const { + if (doc_class_cache.has(p_class_name)) { + return doc_class_cache[p_class_name]; + } + + String description; + HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + if (E) { + // Non-native class shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + description = is_native ? DTR(E->value.brief_description) : E->value.brief_description; + + if (is_native) { + doc_class_cache[p_class_name] = description; + } + } + + return description; +} + +String EditorHelpBit::get_property_description(const StringName &p_class_name, const StringName &p_property_name) const { + if (doc_property_cache.has(p_class_name) && doc_property_cache[p_class_name].has(p_property_name)) { + return doc_property_cache[p_class_name][p_property_name]; + } + + String description; + // Non-native properties shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + DocTools *dd = EditorHelp::get_doc_data(); + HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); + if (E) { + for (int i = 0; i < E->value.properties.size(); i++) { + String description_current = is_native ? DTR(E->value.properties[i].description) : E->value.properties[i].description; + + const Vector<String> class_enum = E->value.properties[i].enumeration.split("."); + const String enum_name = class_enum.size() >= 2 ? class_enum[1] : ""; + if (!enum_name.is_empty()) { + // Classes can use enums from other classes, so check from which it came. + HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_enum[0]); + if (enum_class) { + for (DocData::ConstantDoc val : enum_class->value.constants) { + // Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector. + if (val.enumeration == enum_name && !val.name.ends_with("_MAX")) { + const String enum_value = EditorPropertyNameProcessor::get_singleton()->process_name(val.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED); + const String enum_prefix = EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " "; + const String enum_description = is_native ? DTR(val.description) : val.description; + + // Prettify the enum value display, so that "<ENUM NAME>_<VALUE>" becomes "Value". + description_current = description_current.trim_prefix("\n") + vformat("\n[b]%s:[/b] %s", enum_value.trim_prefix(enum_prefix), enum_description.is_empty() ? ("[i]" + DTR("No description available.") + "[/i]") : enum_description); + } + } + } + } + + if (E->value.properties[i].name == p_property_name) { + description = description_current; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_property_cache[p_class_name][E->value.properties[i].name] = description_current; + } + } + } + + return description; +} + +String EditorHelpBit::get_method_description(const StringName &p_class_name, const StringName &p_method_name) const { + if (doc_method_cache.has(p_class_name) && doc_method_cache[p_class_name].has(p_method_name)) { + return doc_method_cache[p_class_name][p_method_name]; + } + + String description; + HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + if (E) { + // Non-native methods shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + + for (int i = 0; i < E->value.methods.size(); i++) { + String description_current = is_native ? DTR(E->value.methods[i].description) : E->value.methods[i].description; + + if (E->value.methods[i].name == p_method_name) { + description = description_current; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_method_cache[p_class_name][E->value.methods[i].name] = description_current; + } + } + } + + return description; +} + +String EditorHelpBit::get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const { + if (doc_signal_cache.has(p_class_name) && doc_signal_cache[p_class_name].has(p_signal_name)) { + return doc_signal_cache[p_class_name][p_signal_name]; + } + + String description; + HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + if (E) { + // Non-native signals shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + + for (int i = 0; i < E->value.signals.size(); i++) { + String description_current = is_native ? DTR(E->value.signals[i].description) : E->value.signals[i].description; + + if (E->value.signals[i].name == p_signal_name) { + description = description_current; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_signal_cache[p_class_name][E->value.signals[i].name] = description_current; + } + } + } + + return description; +} + +String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const { + if (doc_theme_item_cache.has(p_class_name) && doc_theme_item_cache[p_class_name].has(p_theme_item_name)) { + return doc_theme_item_cache[p_class_name][p_theme_item_name]; + } + + String description; + bool found = false; + DocTools *dd = EditorHelp::get_doc_data(); + HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); + while (E) { + // Non-native theme items shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + + for (int i = 0; i < E->value.theme_properties.size(); i++) { + String description_current = is_native ? DTR(E->value.theme_properties[i].description) : E->value.theme_properties[i].description; + + if (E->value.theme_properties[i].name == p_theme_item_name) { + description = description_current; + found = true; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_theme_item_cache[p_class_name][E->value.theme_properties[i].name] = description_current; + } + } + + if (found || E->value.inherits.is_empty()) { + break; + } + // Check for inherited theme items. + E = dd->class_list.find(E->value.inherits); + } + + return description; +} + void EditorHelpBit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text); ADD_SIGNAL(MethodInfo("request_hide")); @@ -2650,7 +2824,73 @@ EditorHelpBit::EditorHelpBit() { set_custom_minimum_size(Size2(0, 50 * EDSCALE)); } -//// FindBar /// +/// EditorHelpTooltip /// + +void EditorHelpTooltip::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: { + if (!tooltip_text.is_empty()) { + parse_tooltip(tooltip_text); + } + } break; + } +} + +// `p_text` is expected to be something like these: +// - `class|Control||`; +// - `property|Control|size|`; +// - `signal|Control|gui_input|(event: InputEvent)` +void EditorHelpTooltip::parse_tooltip(const String &p_text) { + tooltip_text = p_text; + + PackedStringArray slices = p_text.split("|", true, 3); + ERR_FAIL_COND_MSG(slices.size() < 4, "Invalid tooltip formatting. The expect string should be formatted as 'type|class|property|args'."); + + String type = slices[0]; + String class_name = slices[1]; + String property_name = slices[2]; + String property_args = slices[3]; + + String title; + String description; + String formatted_text; + + if (type == "class") { + title = class_name; + description = get_class_description(class_name); + formatted_text = TTR("Class:"); + } else { + title = property_name; + + if (type == "property") { + description = get_property_description(class_name, property_name); + formatted_text = TTR("Property:"); + } else if (type == "method") { + description = get_method_description(class_name, property_name); + formatted_text = TTR("Method:"); + } else if (type == "signal") { + description = get_signal_description(class_name, property_name); + formatted_text = TTR("Signal:"); + } else if (type == "theme_item") { + description = get_theme_item_description(class_name, property_name); + formatted_text = TTR("Theme Item:"); + } else { + ERR_FAIL_MSG("Invalid tooltip type '" + type + "'. Valid types are 'class', 'property', 'method', 'signal', and 'theme_item'."); + } + } + + formatted_text += " [u][b]" + title + "[/b][/u]" + property_args + "\n"; + formatted_text += description.is_empty() ? "[i]" + TTR("No description available.") + "[/i]" : description; + set_text(formatted_text); +} + +EditorHelpTooltip::EditorHelpTooltip(const String &p_text) { + tooltip_text = p_text; + + get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0)); +} + +/// FindBar /// FindBar::FindBar() { search_text = memnew(LineEdit); diff --git a/editor/editor_help.h b/editor/editor_help.h index 1f1528945b..439b62c34f 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -232,6 +232,12 @@ public: class EditorHelpBit : public MarginContainer { GDCLASS(EditorHelpBit, MarginContainer); + inline static HashMap<StringName, String> doc_class_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_property_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_method_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_signal_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_theme_item_cache; + RichTextLabel *rich_text = nullptr; void _go_to_help(String p_what); void _meta_clicked(String p_select); @@ -243,9 +249,30 @@ protected: void _notification(int p_what); public: + String get_class_description(const StringName &p_class_name) const; + String get_property_description(const StringName &p_class_name, const StringName &p_property_name) const; + String get_method_description(const StringName &p_class_name, const StringName &p_method_name) const; + String get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const; + String get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const; + RichTextLabel *get_rich_text() { return rich_text; } void set_text(const String &p_text); + EditorHelpBit(); }; +class EditorHelpTooltip : public EditorHelpBit { + GDCLASS(EditorHelpTooltip, EditorHelpBit); + + String tooltip_text; + +protected: + void _notification(int p_what); + +public: + void parse_tooltip(const String &p_text); + + EditorHelpTooltip(const String &p_text = String()); +}; + #endif // EDITOR_HELP_H diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 382b182e0e..91a3181747 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -905,47 +905,17 @@ void EditorProperty::_update_pin_flags() { } } -static Control *make_help_bit(const String &p_item_type, const String &p_text, const String &p_warning, const Color &p_warn_color) { - // `p_text` is expected to be something like this: - // `item_name|Item description.`. - // Note that the description can be empty or contain `|`. - PackedStringArray slices = p_text.split("|", true, 1); - if (slices.size() < 2) { - return nullptr; // Use default tooltip instead. - } - - String item_name = slices[0].strip_edges(); - String item_descr = slices[1].strip_edges(); - - String text; - if (!p_item_type.is_empty()) { - text = p_item_type + " "; - } - text += "[u][b]" + item_name + "[/b][/u]\n"; - if (item_descr.is_empty()) { - text += "[i]" + TTR("No description.") + "[/i]"; - } else { - text += item_descr; - } - if (!p_warning.is_empty()) { - text += "\n[b][color=" + p_warn_color.to_html(false) + "]" + p_warning + "[/color][/b]"; - } - - EditorHelpBit *help_bit = memnew(EditorHelpBit); - help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1)); - help_bit->set_text(text); - - return help_bit; -} - Control *EditorProperty::make_custom_tooltip(const String &p_text) const { - String warn; - Color warn_color; + EditorHelpTooltip *tooltip = memnew(EditorHelpTooltip(p_text)); + if (object->has_method("_get_property_warning")) { - warn = object->call("_get_property_warning", property); - warn_color = get_theme_color(SNAME("warning_color")); + String warn = object->call("_get_property_warning", property); + if (!warn.is_empty()) { + tooltip->set_text(tooltip->get_rich_text()->get_text() + "\n[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + warn + "[/color][/b]"); + } } - return make_help_bit(TTR("Property:"), p_text, warn, warn_color); + + return tooltip; } void EditorProperty::menu_option(int p_option) { @@ -1178,7 +1148,8 @@ void EditorInspectorCategory::_notification(int p_what) { } Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const { - return make_help_bit(TTR("Class:"), p_text, String(), Color()); + // Far from perfect solution, as there's nothing that prevents a category from having a name that starts with that. + return p_text.begins_with("class|") ? memnew(EditorHelpTooltip(p_text)) : nullptr; } Size2 EditorInspectorCategory::get_minimum_size() const { @@ -2883,24 +2854,8 @@ void EditorInspector::update_tree() { category->doc_class_name = doc_name; if (use_doc_hints) { - String descr = ""; - // Sets the category tooltip to show documentation. - if (!class_descr_cache.has(doc_name)) { - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(doc_name); - if (E) { - descr = E->value.brief_description; - } - if (ClassDB::class_exists(doc_name)) { - descr = DTR(descr); // Do not translate the class description of scripts. - class_descr_cache[doc_name] = descr; // Do not cache the class description of scripts. - } - } else { - descr = class_descr_cache[doc_name]; - } - - // `|` separator used in `make_help_bit()` for formatting. - category->set_tooltip_text(p.name + "|" + descr); + // `|` separator used in `EditorHelpTooltip` for formatting. + category->set_tooltip_text("class|" + doc_name + "||"); } // Add editors at the start of a category. @@ -3195,13 +3150,12 @@ void EditorInspector::update_tree() { restart_request_props.insert(p.name); } - PropertyDocInfo doc_info; + String doc_path; + String theme_item_name; + StringName classname = doc_name; + // Build the doc hint, to use as tooltip. if (use_doc_hints) { - // Build the doc hint, to use as tooltip. - - // Get the class name. - StringName classname = doc_name; if (!object_class.is_empty()) { classname = object_class; } else if (Object::cast_to<MultiNodeEdit>(object)) { @@ -3231,83 +3185,55 @@ void EditorInspector::update_tree() { classname = get_edited_object()->get_class(); } - // Search for the property description in the cache. - HashMap<StringName, HashMap<StringName, PropertyDocInfo>>::Iterator E = doc_info_cache.find(classname); + // Search for the doc path in the cache. + HashMap<StringName, HashMap<StringName, String>>::Iterator E = doc_path_cache.find(classname); if (E) { - HashMap<StringName, PropertyDocInfo>::Iterator F = E->value.find(propname); + HashMap<StringName, String>::Iterator F = E->value.find(propname); if (F) { found = true; - doc_info = F->value; + doc_path = F->value; } } if (!found) { + DocTools *dd = EditorHelp::get_doc_data(); + // Do not cache the doc path information of scripts. bool is_native_class = ClassDB::class_exists(classname); - // Build the property description String and add it to the cache. - DocTools *dd = EditorHelp::get_doc_data(); HashMap<String, DocData::ClassDoc>::ConstIterator F = dd->class_list.find(classname); - while (F && doc_info.description.is_empty()) { - for (int i = 0; i < F->value.properties.size(); i++) { - if (F->value.properties[i].name == propname.operator String()) { - doc_info.description = F->value.properties[i].description; - if (is_native_class) { - doc_info.description = DTR(doc_info.description); // Do not translate the property description of scripts. - } - - const Vector<String> class_enum = F->value.properties[i].enumeration.split("."); - const String class_name = class_enum[0]; - const String enum_name = class_enum.size() >= 2 ? class_enum[1] : ""; - if (!enum_name.is_empty()) { - HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_name); - if (enum_class) { - for (DocData::ConstantDoc val : enum_class->value.constants) { - // Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector. - if (val.enumeration == enum_name && !val.name.ends_with("_MAX")) { - const String enum_value = EditorPropertyNameProcessor::get_singleton()->process_name(val.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED); - // Prettify the enum value display, so that "<ENUM NAME>_<VALUE>" becomes "Value". - String desc = val.description; - if (is_native_class) { - desc = DTR(desc); // Do not translate the enum value description of scripts. - } - desc = desc.trim_prefix("\n"); - doc_info.description += vformat( - "\n[b]%s:[/b] %s", - enum_value.trim_prefix(EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " "), - desc.is_empty() ? ("[i]" + TTR("No description.") + "[/i]") : desc); - } - } - } - } - - doc_info.path = "class_property:" + F->value.name + ":" + F->value.properties[i].name; - break; - } - } - + while (F) { Vector<String> slices = propname.operator String().split("/"); + // Check if it's a theme item first. if (slices.size() == 2 && slices[0].begins_with("theme_override_")) { for (int i = 0; i < F->value.theme_properties.size(); i++) { + String doc_path_current = "class_theme_item:" + F->value.name + ":" + F->value.theme_properties[i].name; if (F->value.theme_properties[i].name == slices[1]) { - doc_info.description = F->value.theme_properties[i].description; - if (is_native_class) { - doc_info.description = DTR(doc_info.description); // Do not translate the theme item description of scripts. - } - doc_info.path = "class_theme_item:" + F->value.name + ":" + F->value.theme_properties[i].name; - break; + doc_path = doc_path_current; + theme_item_name = F->value.theme_properties[i].name; } } - } - if (!F->value.inherits.is_empty()) { - F = dd->class_list.find(F->value.inherits); + if (is_native_class) { + doc_path_cache[classname][propname] = doc_path; + } } else { - break; + for (int i = 0; i < F->value.properties.size(); i++) { + String doc_path_current = "class_property:" + F->value.name + ":" + F->value.properties[i].name; + if (F->value.properties[i].name == propname.operator String()) { + doc_path = doc_path_current; + } + + if (is_native_class) { + doc_path_cache[classname][propname] = doc_path; + } + } } - } - if (is_native_class) { - doc_info_cache[classname][propname] = doc_info; // Do not cache the doc information of scripts. + if (!doc_path.is_empty() || F->value.inherits.is_empty()) { + break; + } + // Couldn't find the doc path in the class itself, try its super class. + F = dd->class_list.find(F->value.inherits); } } } @@ -3346,11 +3272,11 @@ void EditorInspector::update_tree() { if (properties.size()) { if (properties.size() == 1) { - //since it's one, associate: + // Since it's one, associate: ep->property = properties[0]; ep->property_path = property_prefix + properties[0]; ep->property_usage = p.usage; - //and set label? + // And set label? } if (!editors[i].label.is_empty()) { ep->set_label(editors[i].label); @@ -3398,9 +3324,17 @@ void EditorInspector::update_tree() { ep->connect("multiple_properties_changed", callable_mp(this, &EditorInspector::_multiple_properties_changed)); ep->connect("resource_selected", callable_mp(this, &EditorInspector::_resource_selected), CONNECT_DEFERRED); ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED); - // `|` separator used in `make_help_bit()` for formatting. - ep->set_tooltip_text(property_prefix + p.name + "|" + doc_info.description); - ep->set_doc_path(doc_info.path); + + if (use_doc_hints) { + // `|` separator used in `EditorHelpTooltip` for formatting. + if (theme_item_name.is_empty()) { + ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|"); + } else { + ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name + "|"); + } + } + + ep->set_doc_path(doc_path); ep->update_property(); ep->_update_pin_flags(); ep->update_editor_property_status(); diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index 4393922f52..b5f0cec80b 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -501,13 +501,7 @@ class EditorInspector : public ScrollContainer { int property_focusable; int update_scroll_request; - struct PropertyDocInfo { - String description; - String path; - }; - - HashMap<StringName, HashMap<StringName, PropertyDocInfo>> doc_info_cache; - HashMap<StringName, String> class_descr_cache; + HashMap<StringName, HashMap<StringName, String>> doc_path_cache; HashSet<StringName> restart_request_props; HashMap<ObjectID, int> scroll_cache; diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 9c6dbd333f..0be23fa3cc 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -47,6 +47,7 @@ #include "editor/plugins/script_editor_plugin.h" #include "editor/project_settings_editor.h" #include "editor/property_selector.h" +#include "editor/scene_tree_dock.h" #include "scene/2d/gpu_particles_2d.h" #include "scene/3d/fog_volume.h" #include "scene/3d/gpu_particles_3d.h" @@ -2769,7 +2770,7 @@ EditorPropertyColor::EditorPropertyColor() { void EditorPropertyNodePath::_set_read_only(bool p_read_only) { assign->set_disabled(p_read_only); - clear->set_disabled(p_read_only); + menu->set_disabled(p_read_only); }; Variant EditorPropertyNodePath::_get_cache_value(const StringName &p_prop, bool &r_valid) const { @@ -2817,9 +2818,79 @@ void EditorPropertyNodePath::_node_assign() { scene_tree->popup_scenetree_dialog(); } -void EditorPropertyNodePath::_node_clear() { - emit_changed(get_edited_property(), Variant()); - update_property(); +void EditorPropertyNodePath::_update_menu() { + const NodePath &np = _get_node_path(); + + menu->get_popup()->set_item_disabled(ACTION_CLEAR, np.is_empty()); + menu->get_popup()->set_item_disabled(ACTION_COPY, np.is_empty()); + + Node *edited_node = Object::cast_to<Node>(get_edited_object()); + menu->get_popup()->set_item_disabled(ACTION_SELECT, !edited_node || !edited_node->has_node(np)); +} + +void EditorPropertyNodePath::_menu_option(int p_idx) { + switch (p_idx) { + case ACTION_CLEAR: { + emit_changed(get_edited_property(), NodePath()); + update_property(); + } break; + + case ACTION_COPY: { + DisplayServer::get_singleton()->clipboard_set(_get_node_path()); + } break; + + case ACTION_EDIT: { + assign->hide(); + menu->hide(); + + const NodePath &np = _get_node_path(); + edit->set_text(np); + edit->show(); + callable_mp((Control *)edit, &Control::grab_focus).call_deferred(); + } break; + + case ACTION_SELECT: { + const Node *edited_node = get_base_node(); + ERR_FAIL_NULL(edited_node); + + const NodePath &np = _get_node_path(); + Node *target_node = edited_node->get_node_or_null(np); + ERR_FAIL_NULL(target_node); + + SceneTreeDock::get_singleton()->set_selected(target_node); + } break; + } +} + +void EditorPropertyNodePath::_accept_text() { + _text_submitted(edit->get_text()); +} + +void EditorPropertyNodePath::_text_submitted(const String &p_text) { + NodePath np = p_text; + emit_changed(get_edited_property(), np); + edit->hide(); + assign->show(); + menu->show(); +} + +const NodePath EditorPropertyNodePath::_get_node_path() const { + const Node *base_node = const_cast<EditorPropertyNodePath *>(this)->get_base_node(); + + Variant val = get_edited_property_value(); + Node *n = Object::cast_to<Node>(val); + if (n) { + if (!n->is_inside_tree()) { + return NodePath(); + } + if (base_node) { + return base_node->get_path_to(n); + } else { + return get_tree()->get_edited_scene_root()->get_path_to(n); + } + } else { + return val; + } } bool EditorPropertyNodePath::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { @@ -2865,26 +2936,11 @@ bool EditorPropertyNodePath::is_drop_valid(const Dictionary &p_drag_data) const } void EditorPropertyNodePath::update_property() { - Node *base_node = get_base_node(); - - NodePath p; - Variant val = get_edited_object()->get(get_edited_property()); - Node *n = Object::cast_to<Node>(val); - if (n) { - if (!n->is_inside_tree()) { - return; - } - if (base_node) { - p = base_node->get_path_to(n); - } else { - p = get_tree()->get_edited_scene_root()->get_path_to(n); - } - } else { - p = get_edited_property_value(); - } - + const Node *base_node = get_base_node(); + const NodePath &p = _get_node_path(); assign->set_tooltip_text(p); - if (p == NodePath()) { + + if (p.is_empty()) { assign->set_icon(Ref<Texture2D>()); assign->set_text(TTR("Assign...")); assign->set_flat(false); @@ -2898,7 +2954,7 @@ void EditorPropertyNodePath::update_property() { return; } - Node *target_node = base_node->get_node(p); + const Node *target_node = base_node->get_node(p); ERR_FAIL_NULL(target_node); if (String(target_node->get_name()).contains("@")) { @@ -2922,14 +2978,15 @@ void EditorPropertyNodePath::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: case NOTIFICATION_THEME_CHANGED: { - Ref<Texture2D> t = get_editor_theme_icon(SNAME("Clear")); - clear->set_icon(t); + menu->set_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); + menu->get_popup()->set_item_icon(ACTION_CLEAR, get_editor_theme_icon(SNAME("Clear"))); + menu->get_popup()->set_item_icon(ACTION_COPY, get_editor_theme_icon(SNAME("ActionCopy"))); + menu->get_popup()->set_item_icon(ACTION_EDIT, get_editor_theme_icon(SNAME("Edit"))); + menu->get_popup()->set_item_icon(ACTION_SELECT, get_editor_theme_icon(SNAME("ExternalLink"))); } break; } } -void EditorPropertyNodePath::_bind_methods() { -} Node *EditorPropertyNodePath::get_base_node() { if (!base_hint.is_empty() && get_tree()->get_root()->has_node(base_hint)) { return get_tree()->get_root()->get_node(base_hint); @@ -2974,12 +3031,23 @@ EditorPropertyNodePath::EditorPropertyNodePath() { SET_DRAG_FORWARDING_CD(assign, EditorPropertyNodePath); hbc->add_child(assign); - clear = memnew(Button); - clear->set_flat(true); - clear->connect("pressed", callable_mp(this, &EditorPropertyNodePath::_node_clear)); - hbc->add_child(clear); - - scene_tree = nullptr; //do not allocate unnecessarily + menu = memnew(MenuButton); + menu->set_flat(true); + menu->connect(SNAME("about_to_popup"), callable_mp(this, &EditorPropertyNodePath::_update_menu)); + hbc->add_child(menu); + + menu->get_popup()->add_item(TTR("Clear"), ACTION_CLEAR); + menu->get_popup()->add_item(TTR("Copy as Text"), ACTION_COPY); + menu->get_popup()->add_item(TTR("Edit"), ACTION_EDIT); + menu->get_popup()->add_item(TTR("Show Node in Tree"), ACTION_SELECT); + menu->get_popup()->connect(SNAME("id_pressed"), callable_mp(this, &EditorPropertyNodePath::_menu_option)); + + edit = memnew(LineEdit); + edit->set_h_size_flags(SIZE_EXPAND_FILL); + edit->hide(); + edit->connect(SNAME("focus_exited"), callable_mp(this, &EditorPropertyNodePath::_accept_text)); + edit->connect(SNAME("text_submitted"), callable_mp(this, &EditorPropertyNodePath::_text_submitted)); + hbc->add_child(edit); } ///////////////////// RID ///////////////////////// diff --git a/editor/editor_properties.h b/editor/editor_properties.h index 5feb40b3d7..ff9d47627a 100644 --- a/editor/editor_properties.h +++ b/editor/editor_properties.h @@ -40,6 +40,7 @@ class EditorFileDialog; class EditorLocaleDialog; class EditorResourcePicker; class EditorSpinSlider; +class MenuButton; class PropertySelector; class SceneTreeDialog; class TextEdit; @@ -649,8 +650,18 @@ public: class EditorPropertyNodePath : public EditorProperty { GDCLASS(EditorPropertyNodePath, EditorProperty); + + enum { + ACTION_CLEAR, + ACTION_COPY, + ACTION_EDIT, + ACTION_SELECT, + }; + Button *assign = nullptr; - Button *clear = nullptr; + MenuButton *menu = nullptr; + LineEdit *edit = nullptr; + SceneTreeDialog *scene_tree = nullptr; NodePath base_hint; bool use_path_from_scene_root = false; @@ -659,8 +670,12 @@ class EditorPropertyNodePath : public EditorProperty { Vector<StringName> valid_types; void _node_selected(const NodePath &p_path); void _node_assign(); - void _node_clear(); Node *get_base_node(); + void _update_menu(); + void _menu_option(int p_idx); + void _accept_text(); + void _text_submitted(const String &p_text); + const NodePath _get_node_path() const; bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); @@ -670,7 +685,6 @@ class EditorPropertyNodePath : public EditorProperty { protected: virtual void _set_read_only(bool p_read_only) override; - static void _bind_methods(); void _notification(int p_what); public: diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index 950a1e1c4d..3fad85c95c 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -255,7 +255,7 @@ void EditorPropertyArray::update_property() { array_type_name = vformat("%s[%s]", array_type_name, type_name); } - if (array.get_type() == Variant::NIL) { + if (!array.is_array()) { edit->set_text(vformat(TTR("(Nil) %s"), array_type_name)); edit->set_pressed(false); if (container) { @@ -287,6 +287,7 @@ void EditorPropertyArray::update_property() { if (!container) { container = memnew(MarginContainer); container->set_theme_type_variation("MarginContainer4px"); + container->set_mouse_filter(MOUSE_FILTER_STOP); add_child(container); set_bottom_editor(container); diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 7b48e6fbe9..75c8ac11d0 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -112,6 +112,7 @@ void AnimationPlayerEditor::_notification(int p_what) { } last_active = player->is_playing(); + updating = false; } break; @@ -942,11 +943,6 @@ void AnimationPlayerEditor::_update_player() { onion_toggle->set_disabled(no_anims_found); onion_skinning->set_disabled(no_anims_found); - if (hack_disable_onion_skinning) { - onion_toggle->set_disabled(true); - onion_skinning->set_disabled(true); - } - _update_animation_list_icons(); updating = false; @@ -1150,33 +1146,33 @@ void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay) float alpha_step = 1.0 / (onion.steps + 1); - int cidx = 0; + uint32_t capture_idx = 0; if (onion.past) { - float alpha = 0; + float alpha = 0.0f; do { alpha += alpha_step; - if (onion.captures_valid[cidx]) { + if (onion.captures_valid[capture_idx]) { RS::get_singleton()->canvas_item_add_texture_rect_region( - ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[cidx]), src_rect, Color(1, 1, 1, alpha)); + ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha)); } - cidx++; - } while (cidx < onion.steps); + capture_idx++; + } while (capture_idx < onion.steps); } if (onion.future) { - float alpha = 1; - int base_cidx = cidx; + float alpha = 1.0f; + uint32_t base_cidx = capture_idx; do { alpha -= alpha_step; - if (onion.captures_valid[cidx]) { + if (onion.captures_valid[capture_idx]) { RS::get_singleton()->canvas_item_add_texture_rect_region( - ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[cidx]), src_rect, Color(1, 1, 1, alpha)); + ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha)); } - cidx++; - } while (cidx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it. + capture_idx++; + } while (capture_idx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it. } } @@ -1266,7 +1262,7 @@ void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_set, bool if (!p_timeline_only) { if (player->is_valid() && !p_set) { - double delta = pos - player->get_current_animation_position(); + double delta = player->get_current_animation_position(); player->seek(pos, true, true); player->seek(pos + delta, true, true); } else { @@ -1394,7 +1390,10 @@ void AnimationPlayerEditor::_onion_skinning_menu(int p_option) { onion.enabled = !onion.enabled; if (onion.enabled) { - _start_onion_skinning(); + if (get_player() && !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) { + EditorNode::get_singleton()->show_warning(TTR("Onion skinning requires a RESET animation.")); + } + _start_onion_skinning(); // It will check for RESET animation anyway. } else { _stop_onion_skinning(); } @@ -1416,7 +1415,7 @@ void AnimationPlayerEditor::_onion_skinning_menu(int p_option) { onion.steps = (p_option - ONION_SKINNING_1_STEP) + 1; int one_frame_idx = menu->get_item_index(ONION_SKINNING_1_STEP); for (int i = 0; i <= ONION_SKINNING_LAST_STEPS_OPTION - ONION_SKINNING_1_STEP; i++) { - menu->set_item_checked(one_frame_idx + i, onion.steps == i + 1); + menu->set_item_checked(one_frame_idx + i, (int)onion.steps == i + 1); } } break; case ONION_SKINNING_DIFFERENCES_ONLY: { @@ -1475,15 +1474,15 @@ void AnimationPlayerEditor::_editor_visibility_changed() { bool AnimationPlayerEditor::_are_onion_layers_valid() { ERR_FAIL_COND_V(!onion.past && !onion.future, false); - Point2 capture_size = get_tree()->get_root()->get_size(); - return onion.captures.size() == onion.get_needed_capture_count() && onion.capture_size == capture_size; + Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID); + return onion.captures.size() == onion.get_capture_count() && onion.capture_size == capture_size; } void AnimationPlayerEditor::_allocate_onion_layers() { _free_onion_layers(); - int captures = onion.get_needed_capture_count(); - Point2 capture_size = get_tree()->get_root()->get_size(); + int captures = onion.get_capture_count(); + Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID); onion.captures.resize(captures); onion.captures_valid.resize(captures); @@ -1492,7 +1491,7 @@ void AnimationPlayerEditor::_allocate_onion_layers() { bool is_present = onion.differences_only && i == captures - 1; // Each capture is a viewport with a canvas item attached that renders a full-size rect with the contents of the main viewport. - onion.captures.write[i] = RS::get_singleton()->viewport_create(); + onion.captures[i] = RS::get_singleton()->viewport_create(); RS::get_singleton()->viewport_set_size(onion.captures[i], capture_size.width, capture_size.height); RS::get_singleton()->viewport_set_update_mode(onion.captures[i], RS::VIEWPORT_UPDATE_ALWAYS); @@ -1502,13 +1501,13 @@ void AnimationPlayerEditor::_allocate_onion_layers() { // Reset the capture canvas item to the current root viewport texture (defensive). RS::get_singleton()->canvas_item_clear(onion.capture.canvas_item); - RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), capture_size), get_tree()->get_root()->get_texture()->get_rid()); + RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), Point2(capture_size.x, -capture_size.y)), get_tree()->get_root()->get_texture()->get_rid()); onion.capture_size = capture_size; } void AnimationPlayerEditor::_free_onion_layers() { - for (int i = 0; i < onion.captures.size(); i++) { + for (uint32_t i = 0; i < onion.captures.size(); i++) { if (onion.captures[i].is_valid()) { RS::get_singleton()->free(onion.captures[i]); } @@ -1524,7 +1523,7 @@ void AnimationPlayerEditor::_prepare_onion_layers_1() { return; } - if (!onion.enabled || !is_processing() || !is_visible() || !get_player()) { + if (!onion.enabled || !is_visible() || !get_player() || !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) { _stop_onion_skinning(); return; } @@ -1540,14 +1539,10 @@ void AnimationPlayerEditor::_prepare_onion_layers_1() { } // And go to next step afterwards. - call_deferred(SNAME("_prepare_onion_layers_2")); + callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_prolog).call_deferred(); } -void AnimationPlayerEditor::_prepare_onion_layers_1_deferred() { - call_deferred(SNAME("_prepare_onion_layers_1")); -} - -void AnimationPlayerEditor::_prepare_onion_layers_2() { +void AnimationPlayerEditor::_prepare_onion_layers_2_prolog() { Ref<Animation> anim = player->get_animation(player->get_assigned_animation()); if (!anim.is_valid()) { return; @@ -1558,21 +1553,20 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { } // Hide superfluous elements that would make the overlay unnecessary cluttered. - Dictionary canvas_edit_state; - Dictionary spatial_edit_state; if (Node3DEditor::get_singleton()->is_visible()) { // 3D - spatial_edit_state = Node3DEditor::get_singleton()->get_state(); - Dictionary new_state = spatial_edit_state.duplicate(); + onion.temp.spatial_edit_state = Node3DEditor::get_singleton()->get_state(); + Dictionary new_state = onion.temp.spatial_edit_state.duplicate(); new_state["show_grid"] = false; new_state["show_origin"] = false; - Array orig_vp = spatial_edit_state["viewports"]; + Array orig_vp = onion.temp.spatial_edit_state["viewports"]; Array vp; vp.resize(4); for (int i = 0; i < vp.size(); i++) { Dictionary d = ((Dictionary)orig_vp[i]).duplicate(); d["use_environment"] = false; d["doppler"] = false; + d["listener"] = false; d["gizmos"] = onion.include_gizmos ? d["gizmos"] : Variant(false); d["information"] = false; vp[i] = d; @@ -1580,23 +1574,27 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { new_state["viewports"] = vp; // TODO: Save/restore only affected entries. Node3DEditor::get_singleton()->set_state(new_state); - } else { // CanvasItemEditor - // 2D - canvas_edit_state = CanvasItemEditor::get_singleton()->get_state(); - Dictionary new_state = canvas_edit_state.duplicate(); + } else { + // CanvasItemEditor. + onion.temp.canvas_edit_state = CanvasItemEditor::get_singleton()->get_state(); + Dictionary new_state = onion.temp.canvas_edit_state.duplicate(); + new_state["show_origin"] = false; new_state["show_grid"] = false; new_state["show_rulers"] = false; new_state["show_guides"] = false; new_state["show_helpers"] = false; new_state["show_zoom_control"] = false; + new_state["show_edit_locks"] = false; + new_state["grid_visibility"] = 2; // TODO: Expose CanvasItemEditor::GRID_VISIBILITY_HIDE somehow and use it. + new_state["show_transformation_gizmos"] = onion.include_gizmos ? new_state["gizmos"] : Variant(false); // TODO: Save/restore only affected entries. CanvasItemEditor::get_singleton()->set_state(new_state); } // Tweak the root viewport to ensure it's rendered before our target. RID root_vp = get_tree()->get_root()->get_viewport_rid(); - Rect2 root_vp_screen_rect = Rect2(Vector2(), get_tree()->get_root()->get_size()); - RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2()); + onion.temp.screen_rect = Rect2(Vector2(), DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID)); + RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2(), DisplayServer::INVALID_WINDOW_ID); RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_ALWAYS); RID present_rid; @@ -1611,8 +1609,8 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { } // Backup current animation state. - Ref<AnimatedValuesBackup> backup_current = player->make_backup(); - float cpos = player->get_current_animation_position(); + onion.temp.anim_values_backup = player->make_backup(); + onion.temp.anim_player_position = player->get_current_animation_position(); // Render every past/future step with the capture shader. @@ -1620,55 +1618,94 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { onion.capture.material->set_shader_parameter("bkg_color", GLOBAL_GET("rendering/environment/defaults/default_clear_color")); onion.capture.material->set_shader_parameter("differences_only", onion.differences_only); onion.capture.material->set_shader_parameter("present", onion.differences_only ? RS::get_singleton()->viewport_get_texture(present_rid) : RID()); - - int step_off_a = onion.past ? -onion.steps : 0; - int step_off_b = onion.future ? onion.steps : 0; - int cidx = 0; onion.capture.material->set_shader_parameter("dir_color", onion.force_white_modulate ? Color(1, 1, 1) : Color(EDITOR_GET("editors/animation/onion_layers_past_color"))); - for (int step_off = step_off_a; step_off <= step_off_b; step_off++) { - if (step_off == 0) { - // Skip present step and switch to the color of future. - if (!onion.force_white_modulate) { - onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color")); - } - continue; - } - float pos = cpos + step_off * anim->get_step(); + uint32_t p_capture_idx = 0; + int first_step_offset = onion.past ? -(int)onion.steps : 0; + _prepare_onion_layers_2_step_prepare(first_step_offset, p_capture_idx); +} + +void AnimationPlayerEditor::_prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx) { + uint32_t next_capture_idx = p_capture_idx; + if (p_step_offset == 0) { + // Skip present step and switch to the color of future. + if (!onion.force_white_modulate) { + onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color")); + } + } else { + Ref<Animation> anim = player->get_animation(player->get_assigned_animation()); + double pos = onion.temp.anim_player_position + p_step_offset * anim->get_step(); bool valid = anim->get_loop_mode() != Animation::LOOP_NONE || (pos >= 0 && pos <= anim->get_length()); - onion.captures_valid.write[cidx] = valid; + onion.captures_valid[p_capture_idx] = valid; if (valid) { player->seek(pos, true); - get_tree()->flush_transform_notifications(); // Needed for transforms of Node3Ds. - - RS::get_singleton()->viewport_set_active(onion.captures[cidx], true); - RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[cidx]); - RS::get_singleton()->draw(false); - RS::get_singleton()->viewport_set_active(onion.captures[cidx], false); + OS::get_singleton()->get_main_loop()->process(0); + // This is the key: process the frame and let all callbacks/updates/notifications happen + // so everything (transforms, skeletons, etc.) is up-to-date visually. + callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_step_capture).bind(p_step_offset, p_capture_idx).call_deferred(); + return; + } else { + next_capture_idx++; } + } + + int last_step_offset = onion.future ? onion.steps : 0; + if (p_step_offset < last_step_offset) { + _prepare_onion_layers_2_step_prepare(p_step_offset + 1, next_capture_idx); + } else { + _prepare_onion_layers_2_epilog(); + } +} + +void AnimationPlayerEditor::_prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx) { + DEV_ASSERT(p_step_offset != 0); + DEV_ASSERT(onion.captures_valid[p_capture_idx]); - cidx++; + RID root_vp = get_tree()->get_root()->get_viewport_rid(); + RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], true); + RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[p_capture_idx]); + RS::get_singleton()->draw(false); + RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], false); + + int last_step_offset = onion.future ? onion.steps : 0; + if (p_step_offset < last_step_offset) { + _prepare_onion_layers_2_step_prepare(p_step_offset + 1, p_capture_idx + 1); + } else { + _prepare_onion_layers_2_epilog(); } +} +void AnimationPlayerEditor::_prepare_onion_layers_2_epilog() { // Restore root viewport. + RID root_vp = get_tree()->get_root()->get_viewport_rid(); RS::get_singleton()->viewport_set_parent_viewport(root_vp, RID()); - RS::get_singleton()->viewport_attach_to_screen(root_vp, root_vp_screen_rect); + RS::get_singleton()->viewport_attach_to_screen(root_vp, onion.temp.screen_rect, DisplayServer::MAIN_WINDOW_ID); RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_WHEN_VISIBLE); - // Restore animation state - // (Seeking with update=true wouldn't do the trick because the current value of the properties - // may not match their value for the current point in the animation). - player->seek(cpos, false); - player->restore(backup_current); + // Restore animation state. + // Here we're combine the power of seeking back to the original position and + // restoring the values backup. In most cases they will bring the same value back, + // but there are cases handled by one that the other can't. + // Namely: + // - Seeking won't restore any values that may have been modified by the user + // in the node after the last time the AnimationPlayer updated it. + // - Restoring the backup won't account for values that are not directly involved + // in the animation but a consequence of them (e.g., SkeletonModification2DLookAt). + // FIXME: Since backup of values is based on the reset animation, only values + // backed by a proper reset animation will work correctly with onion + // skinning and the possibility to restore the values mentioned in the + // first point above is gone. Still good enough. + player->seek(onion.temp.anim_player_position, true, true); + player->restore(onion.temp.anim_values_backup); // Restore state of main editors. if (Node3DEditor::get_singleton()->is_visible()) { // 3D - Node3DEditor::get_singleton()->set_state(spatial_edit_state); + Node3DEditor::get_singleton()->set_state(onion.temp.spatial_edit_state); } else { // CanvasItemEditor // 2D - CanvasItemEditor::get_singleton()->set_state(canvas_edit_state); + CanvasItemEditor::get_singleton()->set_state(onion.temp.canvas_edit_state); } // Update viewports with skin layers overlaid for the actual engine loop render. @@ -1677,21 +1714,26 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { } void AnimationPlayerEditor::_start_onion_skinning() { - // FIXME: Using "process_frame" makes onion layers update one frame behind the current. - if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred))) { - get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred)); + if (get_player() && !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) { + onion.enabled = false; + onion_toggle->set_pressed_no_signal(false); + return; + } + if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) { + get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1)); } } void AnimationPlayerEditor::_stop_onion_skinning() { - if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred))) { - get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred)); + if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) { + get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1)); _free_onion_layers(); - // Clean up the overlay. + // Clean up. onion.can_overlay = false; plugin->update_overlays(); + onion.temp = {}; } } @@ -1773,8 +1815,6 @@ void AnimationPlayerEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("_list_changed"), &AnimationPlayerEditor::_list_changed); ClassDB::bind_method(D_METHOD("_animation_duplicate"), &AnimationPlayerEditor::_animation_duplicate); - ClassDB::bind_method(D_METHOD("_prepare_onion_layers_1"), &AnimationPlayerEditor::_prepare_onion_layers_1); - ClassDB::bind_method(D_METHOD("_prepare_onion_layers_2"), &AnimationPlayerEditor::_prepare_onion_layers_2); ClassDB::bind_method(D_METHOD("_start_onion_skinning"), &AnimationPlayerEditor::_start_onion_skinning); ClassDB::bind_method(D_METHOD("_stop_onion_skinning"), &AnimationPlayerEditor::_stop_onion_skinning); @@ -1914,16 +1954,6 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug onion_skinning->get_popup()->add_check_item(TTR("Include Gizmos (3D)"), ONION_SKINNING_INCLUDE_GIZMOS); hb->add_child(onion_skinning); - // FIXME: Onion skinning disabled for now as it's broken and triggers fast - // flickering red/blue modulation (GH-53870). - if (hack_disable_onion_skinning) { - onion_toggle->set_disabled(true); - onion_toggle->set_tooltip_text(TTR("Onion Skinning temporarily disabled due to rendering bug.")); - - onion_skinning->set_disabled(true); - onion_skinning->set_tooltip_text(TTR("Onion Skinning temporarily disabled due to rendering bug.")); - } - hb->add_child(memnew(VSeparator)); pin = memnew(Button); @@ -2013,24 +2043,13 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug track_editor->connect(SNAME("visibility_changed"), callable_mp(this, &AnimationPlayerEditor::_editor_visibility_changed)); - onion.enabled = false; - onion.past = true; - onion.future = false; - onion.steps = 1; - onion.differences_only = false; - onion.force_white_modulate = false; - onion.include_gizmos = false; - - onion.last_frame = 0; - onion.can_overlay = false; - onion.capture_size = Size2(); onion.capture.canvas = RS::get_singleton()->canvas_create(); onion.capture.canvas_item = RS::get_singleton()->canvas_item_create(); RS::get_singleton()->canvas_item_set_parent(onion.capture.canvas_item, onion.capture.canvas); - onion.capture.material = Ref<ShaderMaterial>(memnew(ShaderMaterial)); + onion.capture.material.instantiate(); - onion.capture.shader = Ref<Shader>(memnew(Shader)); + onion.capture.shader.instantiate(); onion.capture.shader->set_code(R"( // Animation editor onion skinning shader. @@ -2047,10 +2066,15 @@ float zero_if_equal(vec4 a, vec4 b) { void fragment() { vec4 capture_samp = texture(TEXTURE, UV); - vec4 present_samp = texture(present, UV); float bkg_mask = zero_if_equal(capture_samp, bkg_color); - float diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color); - diff_mask = min(1.0, diff_mask + float(!differences_only)); + float diff_mask = 1.0; + if (differences_only) { + // FIXME: If Y-flips across render target, canvas item, etc. was handled correctly, + // this would not be as convoluted in the shader. + vec4 capture_samp2 = texture(TEXTURE, vec2(UV.x, 1.0 - UV.y)); + vec4 present_samp = texture(present, vec2(UV.x, 1.0 - UV.y)); + diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color); + } COLOR = vec4(capture_samp.rgb * dir_color.rgb, bkg_mask * diff_mask); } )"); @@ -2061,6 +2085,7 @@ AnimationPlayerEditor::~AnimationPlayerEditor() { _free_onion_layers(); RS::get_singleton()->free(onion.capture.canvas); RS::get_singleton()->free(onion.capture.canvas_item); + onion.capture = {}; } void AnimationPlayerEditorPlugin::_notification(int p_what) { diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index 4763a008fe..6751933839 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -136,20 +136,18 @@ class AnimationPlayerEditor : public VBoxContainer { AnimationTrackEditor *track_editor = nullptr; static AnimationPlayerEditor *singleton; - bool hack_disable_onion_skinning = true; // Temporary hack for GH-53870. - // Onion skinning. struct { // Settings. bool enabled = false; - bool past = false; + bool past = true; bool future = false; - int steps = 0; + uint32_t steps = 1; bool differences_only = false; bool force_white_modulate = false; bool include_gizmos = false; - int get_needed_capture_count() const { + uint32_t get_capture_count() const { // 'Differences only' needs a capture of the present. return (past && future ? 2 * steps : steps) + (differences_only ? 1 : 0); } @@ -158,14 +156,23 @@ class AnimationPlayerEditor : public VBoxContainer { int64_t last_frame = 0; int can_overlay = 0; Size2 capture_size; - Vector<RID> captures; - Vector<bool> captures_valid; + LocalVector<RID> captures; + LocalVector<bool> captures_valid; struct { RID canvas; RID canvas_item; Ref<ShaderMaterial> material; Ref<Shader> shader; } capture; + + // Cross-call state. + struct { + double anim_player_position = 0.0; + Ref<AnimatedValuesBackup> anim_values_backup; + Rect2 screen_rect; + Dictionary canvas_edit_state; + Dictionary spatial_edit_state; + } temp; } onion; void _select_anim_by_name(const String &p_anim); @@ -215,8 +222,10 @@ class AnimationPlayerEditor : public VBoxContainer { void _allocate_onion_layers(); void _free_onion_layers(); void _prepare_onion_layers_1(); - void _prepare_onion_layers_1_deferred(); - void _prepare_onion_layers_2(); + void _prepare_onion_layers_2_prolog(); + void _prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx); + void _prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx); + void _prepare_onion_layers_2_epilog(); void _start_onion_skinning(); void _stop_onion_skinning(); diff --git a/editor/property_selector.cpp b/editor/property_selector.cpp index 5228db03b9..8c0a5b999a 100644 --- a/editor/property_selector.cpp +++ b/editor/property_selector.cpp @@ -370,46 +370,15 @@ void PropertySelector::_item_selected() { class_type = instance->get_class(); } - DocTools *dd = EditorHelp::get_doc_data(); String text; - if (properties) { - while (!class_type.is_empty()) { - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type); - if (E) { - for (int i = 0; i < E->value.properties.size(); i++) { - if (E->value.properties[i].name == name) { - text = DTR(E->value.properties[i].description); - break; - } - } - } - - if (!text.is_empty()) { - break; - } - - // The property may be from a parent class, keep looking. - class_type = ClassDB::get_parent_class(class_type); + while (!class_type.is_empty()) { + text = properties ? help_bit->get_property_description(class_type, name) : help_bit->get_method_description(class_type, name); + if (!text.is_empty()) { + break; } - } else { - while (!class_type.is_empty()) { - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type); - if (E) { - for (int i = 0; i < E->value.methods.size(); i++) { - if (E->value.methods[i].name == name) { - text = DTR(E->value.methods[i].description); - break; - } - } - } - if (!text.is_empty()) { - break; - } - - // The method may be from a parent class, keep looking. - class_type = ClassDB::get_parent_class(class_type); - } + // It may be from a parent class, keep looking. + class_type = ClassDB::get_parent_class(class_type); } if (!text.is_empty()) { diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index e9f55faf7f..cf4354139a 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -142,36 +142,40 @@ void FreeDesktopPortalDesktop::append_dbus_string(DBusMessageIter *p_iter, const } } -void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filters) { +void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts) { DBusMessageIter dict_iter; DBusMessageIter var_iter; DBusMessageIter arr_iter; const char *filters_key = "filters"; + ERR_FAIL_COND(p_filter_names.size() != p_filter_exts.size()); + dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter); dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &filters_key); dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &var_iter); dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "(sa(us))", &arr_iter); - for (int i = 0; i < p_filters.size(); i++) { - Vector<String> tokens = p_filters[i].split(";"); - if (tokens.size() == 2) { - DBusMessageIter struct_iter; - DBusMessageIter array_iter; - DBusMessageIter array_struct_iter; - dbus_message_iter_open_container(&arr_iter, DBUS_TYPE_STRUCT, nullptr, &struct_iter); - append_dbus_string(&struct_iter, tokens[0]); - - dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(us)", &array_iter); + for (int i = 0; i < p_filter_names.size(); i++) { + DBusMessageIter struct_iter; + DBusMessageIter array_iter; + DBusMessageIter array_struct_iter; + dbus_message_iter_open_container(&arr_iter, DBUS_TYPE_STRUCT, nullptr, &struct_iter); + append_dbus_string(&struct_iter, p_filter_names[i]); + + dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(us)", &array_iter); + String flt = p_filter_exts[i]; + int filter_slice_count = flt.get_slice_count(","); + for (int j = 0; j < filter_slice_count; j++) { dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter); + String str = (flt.get_slice(",", j).strip_edges()); { const unsigned nil = 0; dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &nil); } - append_dbus_string(&array_struct_iter, tokens[1]); + append_dbus_string(&array_struct_iter, str); dbus_message_iter_close_container(&array_iter, &array_struct_iter); - dbus_message_iter_close_container(&struct_iter, &array_iter); - dbus_message_iter_close_container(&arr_iter, &struct_iter); } + dbus_message_iter_close_container(&struct_iter, &array_iter); + dbus_message_iter_close_container(&arr_iter, &struct_iter); } dbus_message_iter_close_container(&var_iter, &arr_iter); dbus_message_iter_close_container(&dict_iter, &var_iter); @@ -219,7 +223,7 @@ void FreeDesktopPortalDesktop::append_dbus_dict_bool(DBusMessageIter *p_iter, co dbus_message_iter_close_container(p_iter, &dict_iter); } -bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_iter, bool &r_cancel, Vector<String> &r_urls) { +bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, bool &r_cancel, Vector<String> &r_urls, int &r_index) { ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false); dbus_uint32_t resp_code; @@ -243,7 +247,22 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it DBusMessageIter var_iter; dbus_message_iter_recurse(&iter, &var_iter); - if (strcmp(key, "uris") == 0) { + if (strcmp(key, "current_filter") == 0) { // (sa(us)) + if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_STRUCT) { + DBusMessageIter struct_iter; + dbus_message_iter_recurse(&var_iter, &struct_iter); + while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_STRING) { + const char *value; + dbus_message_iter_get_basic(&struct_iter, &value); + String name = String::utf8(value); + + r_index = p_names.find(name); + if (!dbus_message_iter_next(&struct_iter)) { + break; + } + } + } + } else if (strcmp(key, "uris") == 0) { // as if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_ARRAY) { DBusMessageIter uri_iter; dbus_message_iter_recurse(&var_iter, &uri_iter); @@ -271,6 +290,30 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo return FAILED; } + ERR_FAIL_INDEX_V(int(p_mode), DisplayServer::FILE_DIALOG_MODE_SAVE_MAX, FAILED); + + Vector<String> filter_names; + Vector<String> filter_exts; + for (int i = 0; i < p_filters.size(); i++) { + Vector<String> tokens = p_filters[i].split(";"); + if (tokens.size() >= 1) { + String flt = tokens[0].strip_edges(); + if (!flt.is_empty()) { + if (tokens.size() == 2) { + filter_exts.push_back(flt); + filter_names.push_back(tokens[1]); + } else { + filter_exts.push_back(flt); + filter_names.push_back(flt); + } + } + } + } + if (filter_names.is_empty()) { + filter_exts.push_back("*.*"); + filter_names.push_back(RTR("All Files")); + } + DBusError err; dbus_error_init(&err); @@ -278,6 +321,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo FileDialogData fd; fd.callback = p_callback; fd.prev_focus = p_window_id; + fd.filter_names = filter_names; CryptoCore::RandomGenerator rng; ERR_FAIL_COND_V_MSG(rng.init(), FAILED, "Failed to initialize random number generator."); @@ -308,16 +352,10 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo // Generate FileChooser message. const char *method = nullptr; - switch (p_mode) { - case DisplayServer::FILE_DIALOG_MODE_SAVE_FILE: { - method = "SaveFile"; - } break; - case DisplayServer::FILE_DIALOG_MODE_OPEN_ANY: - case DisplayServer::FILE_DIALOG_MODE_OPEN_FILE: - case DisplayServer::FILE_DIALOG_MODE_OPEN_DIR: - case DisplayServer::FILE_DIALOG_MODE_OPEN_FILES: { - method = "OpenFile"; - } break; + if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) { + method = "SaveFile"; + } else { + method = "OpenFile"; } DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_FILE_CHOOSER, method); @@ -334,7 +372,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo append_dbus_dict_string(&arr_iter, "handle_token", token); append_dbus_dict_bool(&arr_iter, "multiple", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES); append_dbus_dict_bool(&arr_iter, "directory", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR); - append_dbus_dict_filters(&arr_iter, p_filters); + append_dbus_dict_filters(&arr_iter, filter_names, filter_exts); append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true); if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) { append_dbus_dict_string(&arr_iter, "current_name", p_filename); @@ -409,13 +447,15 @@ void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) { if (dbus_message_iter_init(msg, &iter)) { bool cancel = false; Vector<String> uris; - file_chooser_parse_response(&iter, cancel, uris); + int index = 0; + file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index); if (fd.callback.is_valid()) { Variant v_status = !cancel; Variant v_files = uris; - Variant *v_args[2] = { &v_status, &v_files }; - fd.callback.call_deferredp((const Variant **)&v_args, 2); + Variant v_index = index; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; + fd.callback.call_deferredp((const Variant **)&v_args, 3); } if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) { callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus); diff --git a/platform/linuxbsd/freedesktop_portal_desktop.h b/platform/linuxbsd/freedesktop_portal_desktop.h index 6ffb3e7b04..503c382207 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/platform/linuxbsd/freedesktop_portal_desktop.h @@ -49,12 +49,13 @@ private: bool read_setting(const char *p_namespace, const char *p_key, int p_type, void *r_value); static void append_dbus_string(DBusMessageIter *p_iter, const String &p_string); - static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filters); + static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts); static void append_dbus_dict_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array = false); static void append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value); - static bool file_chooser_parse_response(DBusMessageIter *p_iter, bool &r_cancel, Vector<String> &r_urls); + static bool file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, bool &r_cancel, Vector<String> &r_urls, int &r_index); struct FileDialogData { + Vector<String> filter_names; DBusConnection *connection = nullptr; DisplayServer::WindowID prev_focus = DisplayServer::INVALID_WINDOW_ID; Callable callback; diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 7dbe6a5970..2ca9e493b7 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -139,7 +139,14 @@ private: NSMenu *apple_menu = nullptr; NSMenu *dock_menu = nullptr; - HashMap<String, NSMenu *> submenu; + struct MenuData { + Callable open; + Callable close; + NSMenu *menu = nullptr; + bool is_open = false; + }; + HashMap<String, MenuData> submenu; + HashMap<NSMenu *, String> submenu_inv; struct WarpEvent { NSTimeInterval timestamp; @@ -197,6 +204,7 @@ private: const NSMenu *_get_menu_root(const String &p_menu_root) const; NSMenu *_get_menu_root(const String &p_menu_root); + bool _is_menu_opened(NSMenu *p_menu) const; WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect); void _update_window_style(WindowData p_wd); @@ -223,6 +231,8 @@ private: public: NSMenu *get_dock_menu() const; void menu_callback(id p_sender); + void menu_open(NSMenu *p_menu); + void menu_close(NSMenu *p_menu); bool has_window(WindowID p_window) const; WindowData &get_window(WindowID p_window); @@ -254,6 +264,8 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual void global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback = Callable(), const Callable &p_close_callback = Callable()) override; + virtual int global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1) override; virtual int global_menu_add_item(const String &p_menu_root, 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 global_menu_add_check_item(const String &p_menu_root, 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; @@ -277,6 +289,7 @@ public: virtual String global_menu_get_item_submenu(const String &p_menu_root, int p_idx) const override; virtual Key global_menu_get_item_accelerator(const String &p_menu_root, int p_idx) const override; virtual bool global_menu_is_item_disabled(const String &p_menu_root, int p_idx) const override; + virtual bool global_menu_is_item_hidden(const String &p_menu_root, int p_idx) const override; virtual String global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const override; virtual int global_menu_get_item_state(const String &p_menu_root, int p_idx) const override; virtual int global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const override; @@ -288,11 +301,13 @@ public: virtual void global_menu_set_item_radio_checkable(const String &p_menu_root, int p_idx, bool p_checkable) override; virtual void global_menu_set_item_callback(const String &p_menu_root, int p_idx, const Callable &p_callback) override; virtual void global_menu_set_item_key_callback(const String &p_menu_root, int p_idx, const Callable &p_key_callback) override; + virtual void global_menu_set_item_hover_callbacks(const String &p_menu_root, int p_idx, const Callable &p_callback) override; virtual void global_menu_set_item_tag(const String &p_menu_root, int p_idx, const Variant &p_tag) override; virtual void global_menu_set_item_text(const String &p_menu_root, int p_idx, const String &p_text) override; virtual void global_menu_set_item_submenu(const String &p_menu_root, int p_idx, const String &p_submenu) override; virtual void global_menu_set_item_accelerator(const String &p_menu_root, int p_idx, Key p_keycode) override; virtual void global_menu_set_item_disabled(const String &p_menu_root, int p_idx, bool p_disabled) override; + virtual void global_menu_set_item_hidden(const String &p_menu_root, int p_idx, bool p_hidden) override; virtual void global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) override; virtual void global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state) override; virtual void global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states) override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index bcd8f5c4e5..67d6f4214f 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -75,7 +75,7 @@ const NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) cons } else { // Submenu. if (submenu.has(p_menu_root)) { - menu = submenu[p_menu_root]; + menu = submenu[p_menu_root].menu; } } if (menu == apple_menu) { @@ -99,9 +99,10 @@ NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) { NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; [n_menu setAutoenablesItems:NO]; [n_menu setDelegate:menu_delegate]; - submenu[p_menu_root] = n_menu; + submenu[p_menu_root].menu = n_menu; + submenu_inv[n_menu] = p_menu_root; } - menu = submenu[p_menu_root]; + menu = submenu[p_menu_root].menu; } if (menu == apple_menu) { // Do not allow to change Apple menu. @@ -609,6 +610,30 @@ NSMenu *DisplayServerMacOS::get_dock_menu() const { return dock_menu; } +void DisplayServerMacOS::menu_open(NSMenu *p_menu) { + if (submenu_inv.has(p_menu)) { + MenuData &md = submenu[submenu_inv[p_menu]]; + md.is_open = true; + if (md.open.is_valid()) { + Variant ret; + Callable::CallError ce; + md.open.callp(nullptr, 0, ret, ce); + } + } +} + +void DisplayServerMacOS::menu_close(NSMenu *p_menu) { + if (submenu_inv.has(p_menu)) { + MenuData &md = submenu[submenu_inv[p_menu]]; + md.is_open = false; + if (md.close.is_valid()) { + Variant ret; + Callable::CallError ce; + md.close.callp(nullptr, 0, ret, ce); + } + } +} + void DisplayServerMacOS::menu_callback(id p_sender) { if (![p_sender representedObject]) { return; @@ -816,6 +841,24 @@ bool DisplayServerMacOS::_has_help_menu() const { } } +bool DisplayServerMacOS::_is_menu_opened(NSMenu *p_menu) const { + if (submenu_inv.has(p_menu)) { + const MenuData &md = submenu[submenu_inv[p_menu]]; + if (md.is_open) { + return true; + } + } + for (NSInteger i = (p_menu == [NSApp mainMenu]) ? 1 : 0; i < [p_menu numberOfItems]; i++) { + const NSMenuItem *menu_item = [p_menu itemAtIndex:i]; + if ([menu_item submenu]) { + if (_is_menu_opened([menu_item submenu])) { + return true; + } + } + } + return false; +} + NSMenuItem *DisplayServerMacOS::_menu_add_item(const String &p_menu_root, const String &p_label, Key p_accel, int p_index, int *r_out) { NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { @@ -999,6 +1042,23 @@ int DisplayServerMacOS::global_menu_add_multistate_item(const String &p_menu_roo return out; } +void DisplayServerMacOS::global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback, const Callable &p_close_callback) { + _THREAD_SAFE_METHOD_ + + if (p_menu_root != "" && p_menu_root.to_lower() != "_main" && p_menu_root.to_lower() != "_dock") { + // Submenu. + if (!submenu.has(p_menu_root)) { + NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; + [n_menu setAutoenablesItems:NO]; + [n_menu setDelegate:menu_delegate]; + submenu[p_menu_root].menu = n_menu; + submenu_inv[n_menu] = p_menu_root; + } + submenu[p_menu_root].open = p_open_callback; + submenu[p_menu_root].close = p_close_callback; + } +} + int DisplayServerMacOS::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index) { _THREAD_SAFE_METHOD_ @@ -1252,13 +1312,9 @@ String DisplayServerMacOS::global_menu_get_item_submenu(const String &p_menu_roo ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], String()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - const NSMenu *sub_menu = [menu_item submenu]; - if (sub_menu) { - for (const KeyValue<String, NSMenu *> &E : submenu) { - if (E.value == sub_menu) { - return E.key; - } - } + NSMenu *sub_menu = [menu_item submenu]; + if (sub_menu && submenu_inv.has(sub_menu)) { + return submenu_inv[sub_menu]; } } } @@ -1319,6 +1375,24 @@ bool DisplayServerMacOS::global_menu_is_item_disabled(const String &p_menu_root, return false; } +bool DisplayServerMacOS::global_menu_is_item_hidden(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + ERR_FAIL_COND_V(p_idx < 0, false); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], false); + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + return [menu_item isHidden]; + } + } + return false; +} + String DisplayServerMacOS::global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const { _THREAD_SAFE_METHOD_ @@ -1498,6 +1572,25 @@ void DisplayServerMacOS::global_menu_set_item_callback(const String &p_menu_root } } +void DisplayServerMacOS::global_menu_set_item_hover_callbacks(const String &p_menu_root, int p_idx, const Callable &p_callback) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_COND(!obj); + obj->hover_callback = p_callback; + } + } +} + void DisplayServerMacOS::global_menu_set_item_key_callback(const String &p_menu_root, int p_idx, const Callable &p_key_callback) { _THREAD_SAFE_METHOD_ @@ -1557,6 +1650,23 @@ void DisplayServerMacOS::global_menu_set_item_submenu(const String &p_menu_root, _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); + if (menu && p_submenu.is_empty()) { + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + if ([menu_item submenu] && _is_menu_opened([menu_item submenu])) { + ERR_PRINT("Can't remove open menu!"); + return; + } + [menu setSubmenu:nil forItem:menu_item]; + } + return; + } + NSMenu *sub_menu = _get_menu_root(p_submenu); if (menu && sub_menu) { if (sub_menu == menu) { @@ -1591,9 +1701,13 @@ void DisplayServerMacOS::global_menu_set_item_accelerator(const String &p_menu_r ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_keycode)]; - String keycode = KeyMappingMacOS::keycode_get_native_string(p_keycode & KeyModifierMask::CODE_MASK); - [menu_item setKeyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + if (p_keycode == Key::NONE) { + [menu_item setKeyEquivalent:@""]; + } else { + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_keycode)]; + String keycode = KeyMappingMacOS::keycode_get_native_string(p_keycode & KeyModifierMask::CODE_MASK); + [menu_item setKeyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } } } } @@ -1615,6 +1729,23 @@ void DisplayServerMacOS::global_menu_set_item_disabled(const String &p_menu_root } } +void DisplayServerMacOS::global_menu_set_item_hidden(const String &p_menu_root, int p_idx, bool p_hidden) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setHidden:p_hidden]; + } + } +} + void DisplayServerMacOS::global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) { _THREAD_SAFE_METHOD_ @@ -1742,6 +1873,11 @@ void DisplayServerMacOS::global_menu_remove_item(const String &p_menu_root, int p_idx++; } ERR_FAIL_COND(p_idx >= [menu numberOfItems]); + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if ([menu_item submenu] && _is_menu_opened([menu_item submenu])) { + ERR_PRINT("Can't remove open menu!"); + return; + } [menu removeItemAtIndex:p_idx]; } } @@ -1751,6 +1887,10 @@ void DisplayServerMacOS::global_menu_clear(const String &p_menu_root) { NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + if (_is_menu_opened(menu)) { + ERR_PRINT("Can't remove open menu!"); + return; + } [menu removeAllItems]; // Restore Apple menu. if (menu == [NSApp mainMenu]) { @@ -1758,6 +1898,7 @@ void DisplayServerMacOS::global_menu_clear(const String &p_menu_root) { [menu setSubmenu:apple_menu forItem:menu_item]; } if (submenu.has(p_menu_root)) { + submenu_inv.erase(submenu[p_menu_root].menu); submenu.erase(p_menu_root); } } @@ -1871,179 +2012,275 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect return OK; } -Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { - _THREAD_SAFE_METHOD_ +@interface FileDialogDropdown : NSObject { + NSSavePanel *dialog; + NSMutableArray *allowed_types; + int cur_index; +} + +- (instancetype)initWithDialog:(NSSavePanel *)p_dialog fileTypes:(NSMutableArray *)p_allowed_types; +- (void)popupAction:(id)sender; +- (int)getIndex; + +@end + +@implementation FileDialogDropdown + +- (int)getIndex { + return cur_index; +} + +- (instancetype)initWithDialog:(NSSavePanel *)p_dialog fileTypes:(NSMutableArray *)p_allowed_types { + if ((self = [super init])) { + dialog = p_dialog; + allowed_types = p_allowed_types; + cur_index = 0; + } + return self; +} + +- (void)popupAction:(id)sender { + NSUInteger index = [sender indexOfSelectedItem]; + if (index < [allowed_types count]) { + [dialog setAllowedFileTypes:[allowed_types objectAtIndex:index]]; + cur_index = index; + } else { + [dialog setAllowedFileTypes:@[]]; + cur_index = -1; + } +} + +@end + +FileDialogDropdown *_make_accessory_view(NSSavePanel *p_panel, const Vector<String> &p_filters) { + NSView *group = [[NSView alloc] initWithFrame:NSZeroRect]; + group.translatesAutoresizingMaskIntoConstraints = NO; + + NSTextField *label = [NSTextField labelWithString:[NSString stringWithUTF8String:RTR("Format").utf8().get_data()]]; + label.translatesAutoresizingMaskIntoConstraints = NO; + if (@available(macOS 10.14, *)) { + label.textColor = NSColor.secondaryLabelColor; + } + if (@available(macOS 11.10, *)) { + label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]; + } + [group addSubview:label]; + + NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; + popup.translatesAutoresizingMaskIntoConstraints = NO; - NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()]; NSMutableArray *allowed_types = [[NSMutableArray alloc] init]; bool allow_other = false; for (int i = 0; i < p_filters.size(); i++) { Vector<String> tokens = p_filters[i].split(";"); - if (tokens.size() > 0) { - if (tokens[0].strip_edges() == "*.*") { - allow_other = true; - } else { - [allowed_types addObject:[NSString stringWithUTF8String:tokens[0].replace("*.", "").strip_edges().utf8().get_data()]]; + if (tokens.size() >= 1) { + String flt = tokens[0].strip_edges(); + int filter_slice_count = flt.get_slice_count(","); + + NSMutableArray *type_filters = [[NSMutableArray alloc] init]; + for (int j = 0; j < filter_slice_count; j++) { + String str = (flt.get_slice(",", j).strip_edges()); + if (str.strip_edges() == "*.*" || str.strip_edges() == "*") { + allow_other = true; + } else if (!str.is_empty()) { + [type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]]; + } + } + + if ([type_filters count] > 0) { + NSString *name_str = [NSString stringWithUTF8String:((tokens.size() == 1) ? tokens[0] : vformat("%s (%s)", tokens[1], tokens[0])).strip_edges().utf8().get_data()]; + [allowed_types addObject:type_filters]; + [popup addItemWithTitle:name_str]; } } } + FileDialogDropdown *handler = [[FileDialogDropdown alloc] initWithDialog:p_panel fileTypes:allowed_types]; + popup.target = handler; + popup.action = @selector(popupAction:); - WindowID prev_focus = last_focused_window; + [group addSubview:popup]; - Callable callback = p_callback; // Make a copy for async completion handler. - switch (p_mode) { - case FILE_DIALOG_MODE_SAVE_FILE: { - NSSavePanel *panel = [NSSavePanel savePanel]; + NSView *view = [[NSView alloc] initWithFrame:NSZeroRect]; + view.translatesAutoresizingMaskIntoConstraints = NO; + [view addSubview:group]; - [panel setDirectoryURL:[NSURL fileURLWithPath:url]]; - if ([allowed_types count]) { - [panel setAllowedFileTypes:allowed_types]; - } - [panel setAllowsOtherFileTypes:allow_other]; - [panel setExtensionHidden:YES]; - [panel setCanSelectHiddenExtension:YES]; - [panel setCanCreateDirectories:YES]; - [panel setShowsHiddenFiles:p_show_hidden]; - if (p_filename != "") { - NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; - [panel setNameFieldStringValue:fileurl]; - } + NSMutableArray *constraints = [NSMutableArray array]; + [constraints addObject:[popup.topAnchor constraintEqualToAnchor:group.topAnchor constant:10]]; + [constraints addObject:[label.leadingAnchor constraintEqualToAnchor:group.leadingAnchor constant:10]]; + [constraints addObject:[popup.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:10]]; + [constraints addObject:[popup.firstBaselineAnchor constraintEqualToAnchor:label.firstBaselineAnchor]]; + [constraints addObject:[group.trailingAnchor constraintEqualToAnchor:popup.trailingAnchor constant:10]]; + [constraints addObject:[group.bottomAnchor constraintEqualToAnchor:popup.bottomAnchor constant:10]]; + [constraints addObject:[group.topAnchor constraintEqualToAnchor:view.topAnchor]]; + [constraints addObject:[group.centerXAnchor constraintEqualToAnchor:view.centerXAnchor]]; + [constraints addObject:[view.bottomAnchor constraintEqualToAnchor:group.bottomAnchor]]; + [NSLayoutConstraint activateConstraints:constraints]; + + [p_panel setAllowsOtherFileTypes:allow_other]; + if ([allowed_types count] > 0) { + [p_panel setAccessoryView:view]; + [p_panel setAllowedFileTypes:[allowed_types objectAtIndex:0]]; + } + + return handler; +} + +Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED); + + NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()]; + FileDialogDropdown *handler = nullptr; - [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] - completionHandler:^(NSInteger ret) { - if (ret == NSModalResponseOK) { - // Save bookmark for folder. - if (OS::get_singleton()->is_sandboxed()) { - NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + WindowID prev_focus = last_focused_window; + + Callable callback = p_callback; // Make a copy for async completion handler. + if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) { + NSSavePanel *panel = [NSSavePanel savePanel]; + + [panel setDirectoryURL:[NSURL fileURLWithPath:url]]; + handler = _make_accessory_view(panel, p_filters); + [panel setExtensionHidden:YES]; + [panel setCanSelectHiddenExtension:YES]; + [panel setCanCreateDirectories:YES]; + [panel setShowsHiddenFiles:p_show_hidden]; + if (p_filename != "") { + NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; + [panel setNameFieldStringValue:fileurl]; + } + + [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] + completionHandler:^(NSInteger ret) { + if (ret == NSModalResponseOK) { + // Save bookmark for folder. + if (OS::get_singleton()->is_sandboxed()) { + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + bool skip = false; + for (id bookmark in bookmarks) { + NSError *error = nil; + BOOL isStale = NO; + NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; + if (!error && !isStale && ([[exurl path] compare:[[panel directoryURL] path]] == NSOrderedSame)) { + skip = true; + break; + } + } + if (!skip) { + NSError *error = nil; + NSData *bookmark = [[panel directoryURL] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; + if (!error) { + NSArray *new_bookmarks = [bookmarks arrayByAddingObject:bookmark]; + [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; + } + } + } + // Callback. + Vector<String> files; + String url; + url.parse_utf8([[[panel URL] path] UTF8String]); + files.push_back(url); + if (!callback.is_null()) { + Variant v_status = true; + Variant v_files = files; + Variant v_index = [handler getIndex]; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 3, ret, ce); + } + } else { + if (!callback.is_null()) { + Variant v_status = false; + Variant v_files = Vector<String>(); + Variant v_index = [handler getIndex]; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 3, ret, ce); + } + } + if (prev_focus != INVALID_WINDOW_ID) { + callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus); + } + }]; + } else { + NSOpenPanel *panel = [NSOpenPanel openPanel]; + + [panel setDirectoryURL:[NSURL fileURLWithPath:url]]; + handler = _make_accessory_view(panel, p_filters); + [panel setExtensionHidden:YES]; + [panel setCanSelectHiddenExtension:YES]; + [panel setCanCreateDirectories:YES]; + [panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)]; + [panel setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)]; + [panel setShowsHiddenFiles:p_show_hidden]; + if (p_filename != "") { + NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; + [panel setNameFieldStringValue:fileurl]; + } + [panel setAllowsMultipleSelection:(p_mode == FILE_DIALOG_MODE_OPEN_FILES)]; + + [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] + completionHandler:^(NSInteger ret) { + if (ret == NSModalResponseOK) { + // Save bookmark for folder. + NSArray *urls = [(NSOpenPanel *)panel URLs]; + if (OS::get_singleton()->is_sandboxed()) { + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + NSMutableArray *new_bookmarks = [bookmarks mutableCopy]; + for (NSUInteger i = 0; i != [urls count]; ++i) { bool skip = false; for (id bookmark in bookmarks) { NSError *error = nil; BOOL isStale = NO; NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; - if (!error && !isStale && ([[exurl path] compare:[[panel directoryURL] path]] == NSOrderedSame)) { + if (!error && !isStale && ([[exurl path] compare:[[urls objectAtIndex:i] path]] == NSOrderedSame)) { skip = true; break; } } if (!skip) { NSError *error = nil; - NSData *bookmark = [[panel directoryURL] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; + NSData *bookmark = [[urls objectAtIndex:i] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; if (!error) { - NSArray *new_bookmarks = [bookmarks arrayByAddingObject:bookmark]; - [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; + [new_bookmarks addObject:bookmark]; } } } - // Callback. - Vector<String> files; + [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; + } + // Callback. + Vector<String> files; + for (NSUInteger i = 0; i != [urls count]; ++i) { String url; - url.parse_utf8([[[panel URL] path] UTF8String]); + url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]); files.push_back(url); - if (!callback.is_null()) { - Variant v_status = true; - Variant v_files = files; - Variant *v_args[2] = { &v_status, &v_files }; - Variant ret; - Callable::CallError ce; - callback.callp((const Variant **)&v_args, 2, ret, ce); - } - } else { - if (!callback.is_null()) { - Variant v_status = false; - Variant v_files = Vector<String>(); - Variant *v_args[2] = { &v_status, &v_files }; - Variant ret; - Callable::CallError ce; - callback.callp((const Variant **)&v_args, 2, ret, ce); - } } - if (prev_focus != INVALID_WINDOW_ID) { - callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus); + if (!callback.is_null()) { + Variant v_status = true; + Variant v_files = files; + Variant v_index = [handler getIndex]; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 3, ret, ce); } - }]; - } break; - case FILE_DIALOG_MODE_OPEN_ANY: - case FILE_DIALOG_MODE_OPEN_FILE: - case FILE_DIALOG_MODE_OPEN_FILES: - case FILE_DIALOG_MODE_OPEN_DIR: { - NSOpenPanel *panel = [NSOpenPanel openPanel]; - - [panel setDirectoryURL:[NSURL fileURLWithPath:url]]; - if ([allowed_types count]) { - [panel setAllowedFileTypes:allowed_types]; - } - [panel setAllowsOtherFileTypes:allow_other]; - [panel setExtensionHidden:YES]; - [panel setCanSelectHiddenExtension:YES]; - [panel setCanCreateDirectories:YES]; - [panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)]; - [panel setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)]; - [panel setShowsHiddenFiles:p_show_hidden]; - if (p_filename != "") { - NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; - [panel setNameFieldStringValue:fileurl]; - } - [panel setAllowsMultipleSelection:(p_mode == FILE_DIALOG_MODE_OPEN_FILES)]; - - [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] - completionHandler:^(NSInteger ret) { - if (ret == NSModalResponseOK) { - // Save bookmark for folder. - NSArray *urls = [(NSOpenPanel *)panel URLs]; - if (OS::get_singleton()->is_sandboxed()) { - NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; - NSMutableArray *new_bookmarks = [bookmarks mutableCopy]; - for (NSUInteger i = 0; i != [urls count]; ++i) { - bool skip = false; - for (id bookmark in bookmarks) { - NSError *error = nil; - BOOL isStale = NO; - NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; - if (!error && !isStale && ([[exurl path] compare:[[urls objectAtIndex:i] path]] == NSOrderedSame)) { - skip = true; - break; - } - } - if (!skip) { - NSError *error = nil; - NSData *bookmark = [[urls objectAtIndex:i] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; - if (!error) { - [new_bookmarks addObject:bookmark]; - } - } - } - [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; - } - // Callback. - Vector<String> files; - for (NSUInteger i = 0; i != [urls count]; ++i) { - String url; - url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]); - files.push_back(url); - } - if (!callback.is_null()) { - Variant v_status = true; - Variant v_files = files; - Variant *v_args[2] = { &v_status, &v_files }; - Variant ret; - Callable::CallError ce; - callback.callp((const Variant **)&v_args, 2, ret, ce); - } - } else { - if (!callback.is_null()) { - Variant v_status = false; - Variant v_files = Vector<String>(); - Variant *v_args[2] = { &v_status, &v_files }; - Variant ret; - Callable::CallError ce; - callback.callp((const Variant **)&v_args, 2, ret, ce); - } - } - if (prev_focus != INVALID_WINDOW_ID) { - callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus); + } else { + if (!callback.is_null()) { + Variant v_status = false; + Variant v_files = Vector<String>(); + Variant v_index = [handler getIndex]; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 3, ret, ce); } - }]; - } break; + } + if (prev_focus != INVALID_WINDOW_ID) { + callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus); + } + }]; } return OK; @@ -4188,15 +4425,19 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM nsappname = [[NSProcessInfo processInfo] processName]; } + menu_delegate = [[GodotMenuDelegate alloc] init]; + // Setup Dock menu. dock_menu = [[NSMenu alloc] initWithTitle:@"_dock"]; [dock_menu setAutoenablesItems:NO]; + [dock_menu setDelegate:menu_delegate]; // Setup Apple menu. apple_menu = [[NSMenu alloc] initWithTitle:@""]; title = [NSString stringWithFormat:NSLocalizedString(@"About %@", nil), nsappname]; [apple_menu addItemWithTitle:title action:@selector(showAbout:) keyEquivalent:@""]; [apple_menu setAutoenablesItems:NO]; + [apple_menu setDelegate:menu_delegate]; [apple_menu addItem:[NSMenuItem separatorItem]]; @@ -4226,8 +4467,6 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM [main_menu setSubmenu:apple_menu forItem:menu_item]; [main_menu setAutoenablesItems:NO]; - menu_delegate = [[GodotMenuDelegate alloc] init]; - //!!!!!!!!!!!!!!!!!!!!!!!!!! //TODO - do Vulkan and OpenGL support checks, driver selection and fallback rendering_driver = p_rendering_driver; diff --git a/platform/macos/godot_menu_delegate.mm b/platform/macos/godot_menu_delegate.mm index ebfe8b1f6d..1bfcfa7d9e 100644 --- a/platform/macos/godot_menu_delegate.mm +++ b/platform/macos/godot_menu_delegate.mm @@ -39,6 +39,34 @@ - (void)doNothing:(id)sender { } +- (void)menuNeedsUpdate:(NSMenu *)menu { + if (DisplayServer::get_singleton()) { + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + ds->menu_open(menu); + } +} + +- (void)menuDidClose:(NSMenu *)menu { + if (DisplayServer::get_singleton()) { + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + ds->menu_close(menu); + } +} + +- (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item { + if (item) { + GodotMenuItem *value = [item representedObject]; + if (value && value->hover_callback != Callable()) { + // If custom callback is set, use it. + Variant tag = value->meta; + Variant *tagp = &tag; + Variant ret; + Callable::CallError ce; + value->hover_callback.callp((const Variant **)&tagp, 1, ret, ce); + } + } +} + - (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id *)target action:(SEL *)action { NSString *ev_key = [[event charactersIgnoringModifiers] lowercaseString]; NSUInteger ev_modifiers = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h index 8876ceae6a..b816ea1b3a 100644 --- a/platform/macos/godot_menu_item.h +++ b/platform/macos/godot_menu_item.h @@ -46,6 +46,7 @@ enum GlobalMenuCheckType { @public Callable callback; Callable key_callback; + Callable hover_callback; Variant meta; GlobalMenuCheckType checkable_type; int max_states; diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index ed52c5eb92..ded80ba5f1 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -222,18 +222,37 @@ void DisplayServerWindows::tts_stop() { Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { _THREAD_SAFE_METHOD_ + ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED); + Vector<Char16String> filter_names; Vector<Char16String> filter_exts; for (const String &E : p_filters) { Vector<String> tokens = E.split(";"); - if (tokens.size() == 2) { - filter_exts.push_back(tokens[0].strip_edges().utf16()); - filter_names.push_back(tokens[1].strip_edges().utf16()); - } else if (tokens.size() == 1) { - filter_exts.push_back(tokens[0].strip_edges().utf16()); - filter_names.push_back(tokens[0].strip_edges().utf16()); + if (tokens.size() >= 1) { + String flt = tokens[0].strip_edges(); + int filter_slice_count = flt.get_slice_count(","); + Vector<String> exts; + for (int j = 0; j < filter_slice_count; j++) { + String str = (flt.get_slice(",", j).strip_edges()); + if (!str.is_empty()) { + exts.push_back(str); + } + } + if (!exts.is_empty()) { + String str = String(";").join(exts); + filter_exts.push_back(str.utf16()); + if (tokens.size() == 2) { + filter_names.push_back(tokens[1].strip_edges().utf16()); + } else { + filter_names.push_back(str.utf16()); + } + } } } + if (filter_names.is_empty()) { + filter_exts.push_back(String("*.*").utf16()); + filter_names.push_back(RTR("All Files").utf16()); + } Vector<COMDLG_FILTERSPEC> filters; for (int i = 0; i < filter_names.size(); i++) { @@ -287,6 +306,9 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String } hr = pfd->Show(windows[window_id].hWnd); + UINT index = 0; + pfd->GetFileTypeIndex(&index); + if (SUCCEEDED(hr)) { Vector<String> file_names; @@ -326,19 +348,21 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String if (!p_callback.is_null()) { Variant v_status = true; Variant v_files = file_names; - Variant *v_args[2] = { &v_status, &v_files }; + Variant v_index = index; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; Variant ret; Callable::CallError ce; - p_callback.callp((const Variant **)&v_args, 2, ret, ce); + p_callback.callp((const Variant **)&v_args, 3, ret, ce); } } else { if (!p_callback.is_null()) { Variant v_status = false; Variant v_files = Vector<String>(); - Variant *v_args[2] = { &v_status, &v_files }; + Variant v_index = index; + Variant *v_args[3] = { &v_status, &v_files, &v_index }; Variant ret; Callable::CallError ce; - p_callback.callp((const Variant **)&v_args, 2, ret, ce); + p_callback.callp((const Variant **)&v_args, 3, ret, ce); } } pfd->Release(); diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 3857281a66..b4649c2401 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -73,7 +73,7 @@ void FileDialog::set_visible(bool p_visible) { } } -void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files) { +void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter) { if (p_ok) { if (p_files.size() > 0) { String f = p_files[0]; @@ -90,6 +90,7 @@ void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files) { } file->set_text(f); dir->set_text(f.get_base_dir()); + _filter_selected(p_filter); } } else { file->set_text(""); diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 1a87b79fdd..8ae84fc9dc 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -159,7 +159,7 @@ private: virtual void shortcut_input(const Ref<InputEvent> &p_event) override; - void _native_dialog_cb(bool p_ok, const Vector<String> &p_files); + void _native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter); bool _is_open_should_be_disabled(); diff --git a/scene/gui/menu_bar.cpp b/scene/gui/menu_bar.cpp index 13a42d0407..371d6c69af 100644 --- a/scene/gui/menu_bar.cpp +++ b/scene/gui/menu_bar.cpp @@ -202,52 +202,6 @@ void MenuBar::_popup_visibility_changed(bool p_visible) { } } -void MenuBar::_update_submenu(const String &p_menu_name, PopupMenu *p_child) { - int count = p_child->get_item_count(); - global_menus.insert(p_menu_name); - for (int i = 0; i < count; i++) { - if (p_child->is_item_separator(i)) { - DisplayServer::get_singleton()->global_menu_add_separator(p_menu_name); - } else if (!p_child->get_item_submenu(i).is_empty()) { - Node *n = p_child->get_node_or_null(p_child->get_item_submenu(i)); - ERR_FAIL_NULL_MSG(n, "Item subnode does not exist: '" + p_child->get_item_submenu(i) + "'."); - PopupMenu *pm = Object::cast_to<PopupMenu>(n); - ERR_FAIL_NULL_MSG(pm, "Item subnode is not a PopupMenu: '" + p_child->get_item_submenu(i) + "'."); - - DisplayServer::get_singleton()->global_menu_add_submenu_item(p_menu_name, atr(p_child->get_item_text(i)), p_menu_name + "/" + itos(i)); - _update_submenu(p_menu_name + "/" + itos(i), pm); - } else { - int index = DisplayServer::get_singleton()->global_menu_add_item(p_menu_name, atr(p_child->get_item_text(i)), callable_mp(p_child, &PopupMenu::activate_item), Callable(), i); - - if (p_child->is_item_checkable(i)) { - DisplayServer::get_singleton()->global_menu_set_item_checkable(p_menu_name, index, true); - } - if (p_child->is_item_radio_checkable(i)) { - DisplayServer::get_singleton()->global_menu_set_item_radio_checkable(p_menu_name, index, true); - } - DisplayServer::get_singleton()->global_menu_set_item_checked(p_menu_name, index, p_child->is_item_checked(i)); - DisplayServer::get_singleton()->global_menu_set_item_disabled(p_menu_name, index, p_child->is_item_disabled(i)); - DisplayServer::get_singleton()->global_menu_set_item_max_states(p_menu_name, index, p_child->get_item_max_states(i)); - DisplayServer::get_singleton()->global_menu_set_item_icon(p_menu_name, index, p_child->get_item_icon(i)); - DisplayServer::get_singleton()->global_menu_set_item_state(p_menu_name, index, p_child->get_item_state(i)); - DisplayServer::get_singleton()->global_menu_set_item_indentation_level(p_menu_name, index, p_child->get_item_indent(i)); - DisplayServer::get_singleton()->global_menu_set_item_tooltip(p_menu_name, index, p_child->get_item_tooltip(i)); - if (!p_child->is_item_shortcut_disabled(i) && p_child->get_item_shortcut(i).is_valid() && p_child->get_item_shortcut(i)->has_valid_event()) { - Array events = p_child->get_item_shortcut(i)->get_events(); - for (int j = 0; j < events.size(); j++) { - Ref<InputEventKey> ie = events[j]; - if (ie.is_valid()) { - DisplayServer::get_singleton()->global_menu_set_item_accelerator(p_menu_name, index, ie->get_keycode_with_modifiers()); - break; - } - } - } else if (p_child->get_item_accelerator(i) != Key::NONE) { - DisplayServer::get_singleton()->global_menu_set_item_accelerator(p_menu_name, i, p_child->get_item_accelerator(i)); - } - } - } -} - bool MenuBar::is_native_menu() const { #ifdef TOOLS_ENABLED if (is_part_of_edited_scene()) { @@ -258,52 +212,67 @@ bool MenuBar::is_native_menu() const { return (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU) && is_native); } -void MenuBar::_clear_menu() { +String MenuBar::bind_global_menu() { +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene()) { + return String(); + } +#endif if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) { - return; + return String(); } - // Remove root menu items. - int count = DisplayServer::get_singleton()->global_menu_get_item_count("_main"); - for (int i = count - 1; i >= 0; i--) { - if (global_menus.has(DisplayServer::get_singleton()->global_menu_get_item_submenu("_main", i))) { - DisplayServer::get_singleton()->global_menu_remove_item("_main", i); + if (!global_menu_name.is_empty()) { + return global_menu_name; // Already bound. + } + + DisplayServer *ds = DisplayServer::get_singleton(); + global_menu_name = "__MenuBar#" + itos(get_instance_id()); + + int global_start_idx = -1; + int count = ds->global_menu_get_item_count("_main"); + String prev_tag; + for (int i = 0; i < count; i++) { + String tag = ds->global_menu_get_item_tag("_main", i).operator String().get_slice("#", 1); + if (!tag.is_empty() && tag != prev_tag) { + if (i >= start_index) { + global_start_idx = i; + break; + } } + prev_tag = tag; } - // Erase submenu contents. - for (const String &E : global_menus) { - DisplayServer::get_singleton()->global_menu_clear(E); + if (global_start_idx == -1) { + global_start_idx = count; } - global_menus.clear(); -} -void MenuBar::_update_menu() { - _clear_menu(); + Vector<PopupMenu *> popups = _get_popups(); + for (int i = 0; i < menu_cache.size(); i++) { + String submenu_name = popups[i]->bind_global_menu(); + int index = ds->global_menu_add_submenu_item("_main", menu_cache[i].name, submenu_name, global_start_idx + i); + ds->global_menu_set_item_tag("_main", index, global_menu_name + "#" + itos(i)); + ds->global_menu_set_item_hidden("_main", index, menu_cache[i].hidden); + ds->global_menu_set_item_disabled("_main", index, menu_cache[i].disabled); + ds->global_menu_set_item_tooltip("_main", index, menu_cache[i].tooltip); + } + + return global_menu_name; +} - if (!is_visible_in_tree()) { +void MenuBar::unbind_global_menu() { + if (global_menu_name.is_empty()) { return; } - int index = start_index; - if (is_native_menu()) { - Vector<PopupMenu *> popups = _get_popups(); - String root_name = "MenuBar<" + String::num_int64((uint64_t)this, 16) + ">"; - for (int i = 0; i < popups.size(); i++) { - if (menu_cache[i].hidden) { - continue; - } - String menu_name = atr(String(popups[i]->get_meta("_menu_name", popups[i]->get_name()))); - - index = DisplayServer::get_singleton()->global_menu_add_submenu_item("_main", menu_name, root_name + "/" + itos(i), index); - if (menu_cache[i].disabled) { - DisplayServer::get_singleton()->global_menu_set_item_disabled("_main", index, true); - } - _update_submenu(root_name + "/" + itos(i), popups[i]); - index++; - } + DisplayServer *ds = DisplayServer::get_singleton(); + int global_start = _find_global_start_index(); + Vector<PopupMenu *> popups = _get_popups(); + for (int i = menu_cache.size() - 1; i >= 0; i--) { + popups[i]->unbind_global_menu(); + ds->global_menu_remove_item("_main", global_start + i); } - update_minimum_size(); - queue_redraw(); + + global_menu_name = String(); } void MenuBar::_notification(int p_what) { @@ -312,25 +281,43 @@ void MenuBar::_notification(int p_what) { if (get_menu_count() > 0) { _refresh_menu_names(); } + if (is_native_menu()) { + bind_global_menu(); + } } break; case NOTIFICATION_EXIT_TREE: { - _clear_menu(); + unbind_global_menu(); } break; case NOTIFICATION_MOUSE_EXIT: { focused_menu = -1; selected_menu = -1; queue_redraw(); } break; - case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + DisplayServer *ds = DisplayServer::get_singleton(); + bool is_global = !global_menu_name.is_empty(); + int global_start = _find_global_start_index(); + for (int i = 0; i < menu_cache.size(); i++) { + shape(menu_cache.write[i]); + if (is_global) { + ds->global_menu_set_item_text("_main", global_start + i, atr(menu_cache[i].name)); + } + } + } break; case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { for (int i = 0; i < menu_cache.size(); i++) { shape(menu_cache.write[i]); } - _update_menu(); } break; case NOTIFICATION_VISIBILITY_CHANGED: { - _update_menu(); + if (is_native_menu()) { + if (is_visible_in_tree()) { + bind_global_menu(); + } else { + unbind_global_menu(); + } + } } break; case NOTIFICATION_DRAW: { if (is_native_menu()) { @@ -512,14 +499,20 @@ void MenuBar::shape(Menu &p_menu) { } void MenuBar::_refresh_menu_names() { + DisplayServer *ds = DisplayServer::get_singleton(); + bool is_global = !global_menu_name.is_empty(); + int global_start = _find_global_start_index(); + Vector<PopupMenu *> popups = _get_popups(); for (int i = 0; i < popups.size(); i++) { if (!popups[i]->has_meta("_menu_name") && String(popups[i]->get_name()) != get_menu_title(i)) { menu_cache.write[i].name = popups[i]->get_name(); shape(menu_cache.write[i]); + if (is_global) { + ds->global_menu_set_item_text("_main", global_start + i, atr(menu_cache[i].name)); + } } } - _update_menu(); } Vector<PopupMenu *> MenuBar::_get_popups() const { @@ -560,11 +553,14 @@ void MenuBar::add_child_notify(Node *p_child) { menu_cache.push_back(menu); p_child->connect("renamed", callable_mp(this, &MenuBar::_refresh_menu_names)); - p_child->connect("menu_changed", callable_mp(this, &MenuBar::_update_menu)); p_child->connect("about_to_popup", callable_mp(this, &MenuBar::_popup_visibility_changed).bind(true)); p_child->connect("popup_hide", callable_mp(this, &MenuBar::_popup_visibility_changed).bind(false)); - _update_menu(); + if (!global_menu_name.is_empty()) { + String submenu_name = pm->bind_global_menu(); + int index = DisplayServer::get_singleton()->global_menu_add_submenu_item("_main", atr(menu.name), submenu_name, _find_global_start_index() + menu_cache.size() - 1); + DisplayServer::get_singleton()->global_menu_set_item_tag("_main", index, global_menu_name + "#" + itos(menu_cache.size() - 1)); + } } void MenuBar::move_child_notify(Node *p_child) { @@ -586,9 +582,20 @@ void MenuBar::move_child_notify(Node *p_child) { } Menu menu = menu_cache[old_idx]; menu_cache.remove_at(old_idx); - menu_cache.insert(get_menu_idx_from_control(pm), menu); + int new_idx = get_menu_idx_from_control(pm); + menu_cache.insert(new_idx, menu); - _update_menu(); + if (!global_menu_name.is_empty()) { + int global_start = _find_global_start_index(); + if (old_idx != -1) { + DisplayServer::get_singleton()->global_menu_remove_item("_main", global_start + old_idx); + } + if (new_idx != -1) { + String submenu_name = pm->bind_global_menu(); + int index = DisplayServer::get_singleton()->global_menu_add_submenu_item("_main", atr(menu.name), submenu_name, global_start + new_idx); + DisplayServer::get_singleton()->global_menu_set_item_tag("_main", index, global_menu_name + "#" + itos(new_idx)); + } + } } void MenuBar::remove_child_notify(Node *p_child) { @@ -603,15 +610,17 @@ void MenuBar::remove_child_notify(Node *p_child) { menu_cache.remove_at(idx); + if (!global_menu_name.is_empty()) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->global_menu_remove_item("_main", _find_global_start_index() + idx); + } + p_child->remove_meta("_menu_name"); p_child->remove_meta("_menu_tooltip"); p_child->disconnect("renamed", callable_mp(this, &MenuBar::_refresh_menu_names)); - p_child->disconnect("menu_changed", callable_mp(this, &MenuBar::_update_menu)); p_child->disconnect("about_to_popup", callable_mp(this, &MenuBar::_popup_visibility_changed)); p_child->disconnect("popup_hide", callable_mp(this, &MenuBar::_popup_visibility_changed)); - - _update_menu(); } void MenuBar::_bind_methods() { @@ -699,7 +708,8 @@ void MenuBar::set_text_direction(Control::TextDirection p_text_direction) { ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); if (text_direction != p_text_direction) { text_direction = p_text_direction; - _update_menu(); + update_minimum_size(); + queue_redraw(); } } @@ -710,7 +720,8 @@ Control::TextDirection MenuBar::get_text_direction() const { void MenuBar::set_language(const String &p_language) { if (language != p_language) { language = p_language; - _update_menu(); + update_minimum_size(); + queue_redraw(); } } @@ -732,7 +743,10 @@ bool MenuBar::is_flat() const { void MenuBar::set_start_index(int p_index) { if (start_index != p_index) { start_index = p_index; - _update_menu(); + if (!global_menu_name.is_empty()) { + unbind_global_menu(); + bind_global_menu(); + } } } @@ -742,11 +756,12 @@ int MenuBar::get_start_index() const { void MenuBar::set_prefer_global_menu(bool p_enabled) { if (is_native != p_enabled) { + is_native = p_enabled; if (is_native) { - _clear_menu(); + bind_global_menu(); + } else { + unbind_global_menu(); } - is_native = p_enabled; - _update_menu(); } } @@ -790,7 +805,9 @@ void MenuBar::set_menu_title(int p_menu, const String &p_title) { } menu_cache.write[p_menu].name = p_title; shape(menu_cache.write[p_menu]); - _update_menu(); + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_text("_main", _find_global_start_index() + p_menu, atr(menu_cache[p_menu].name)); + } } String MenuBar::get_menu_title(int p_menu) const { @@ -802,7 +819,10 @@ void MenuBar::set_menu_tooltip(int p_menu, const String &p_tooltip) { ERR_FAIL_INDEX(p_menu, menu_cache.size()); PopupMenu *pm = get_menu_popup(p_menu); pm->set_meta("_menu_tooltip", p_tooltip); - menu_cache.write[p_menu].name = p_tooltip; + menu_cache.write[p_menu].tooltip = p_tooltip; + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_tooltip("_main", _find_global_start_index() + p_menu, p_tooltip); + } } String MenuBar::get_menu_tooltip(int p_menu) const { @@ -813,7 +833,9 @@ String MenuBar::get_menu_tooltip(int p_menu) const { void MenuBar::set_menu_disabled(int p_menu, bool p_disabled) { ERR_FAIL_INDEX(p_menu, menu_cache.size()); menu_cache.write[p_menu].disabled = p_disabled; - _update_menu(); + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_disabled("_main", _find_global_start_index() + p_menu, p_disabled); + } } bool MenuBar::is_menu_disabled(int p_menu) const { @@ -824,7 +846,9 @@ bool MenuBar::is_menu_disabled(int p_menu) const { void MenuBar::set_menu_hidden(int p_menu, bool p_hidden) { ERR_FAIL_INDEX(p_menu, menu_cache.size()); menu_cache.write[p_menu].hidden = p_hidden; - _update_menu(); + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_hidden("_main", _find_global_start_index() + p_menu, p_hidden); + } } bool MenuBar::is_menu_hidden(int p_menu) const { diff --git a/scene/gui/menu_bar.h b/scene/gui/menu_bar.h index 4d6e76d9b6..ba4df5f229 100644 --- a/scene/gui/menu_bar.h +++ b/scene/gui/menu_bar.h @@ -66,7 +66,6 @@ class MenuBar : public Control { } }; Vector<Menu> menu_cache; - HashSet<String> global_menus; int focused_menu = -1; int selected_menu = -1; @@ -114,9 +113,23 @@ class MenuBar : public Control { void _open_popup(int p_index, bool p_focus_item = false); void _popup_visibility_changed(bool p_visible); - void _update_submenu(const String &p_menu_name, PopupMenu *p_child); - void _clear_menu(); - void _update_menu(); + + String global_menu_name; + + int _find_global_start_index() { + if (global_menu_name.is_empty()) { + return -1; + } + + DisplayServer *ds = DisplayServer::get_singleton(); + int count = ds->global_menu_get_item_count("_main"); + for (int i = 0; i < count; i++) { + if (ds->global_menu_get_item_tag("_main", i).operator String().begins_with(global_menu_name)) { + return i; + } + } + return -1; + } protected: virtual void shortcut_input(const Ref<InputEvent> &p_event) override; @@ -130,6 +143,9 @@ protected: public: virtual void gui_input(const Ref<InputEvent> &p_event) override; + String bind_global_menu(); + void unbind_global_menu(); + void set_switch_on_hover(bool p_enabled); bool is_switch_on_hover(); void set_disable_shortcuts(bool p_disabled); diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 54fd8b8745..e3b0a18325 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -40,6 +40,86 @@ #include "scene/gui/menu_bar.h" #include "scene/theme/theme_db.h" +String PopupMenu::bind_global_menu() { +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene()) { + return String(); + } +#endif + if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) { + return String(); + } + + if (!global_menu_name.is_empty()) { + return global_menu_name; // Already bound; + } + + DisplayServer *ds = DisplayServer::get_singleton(); + global_menu_name = "__PopupMenu#" + itos(get_instance_id()); + ds->global_menu_set_popup_callbacks(global_menu_name, callable_mp(this, &PopupMenu::_about_to_popup), callable_mp(this, &PopupMenu::_about_to_close)); + for (int i = 0; i < items.size(); i++) { + Item &item = items.write[i]; + if (item.separator) { + ds->global_menu_add_separator(global_menu_name); + } else { + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), i); + if (!item.submenu.is_empty()) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(item.submenu)); + if (pm) { + String submenu_name = pm->bind_global_menu(); + ds->global_menu_set_item_submenu(global_menu_name, index, submenu_name); + item.submenu_bound = true; + } + } + if (item.checkable_type == Item::CHECKABLE_TYPE_CHECK_BOX) { + ds->global_menu_set_item_checkable(global_menu_name, index, true); + } else if (item.checkable_type == Item::CHECKABLE_TYPE_RADIO_BUTTON) { + ds->global_menu_set_item_radio_checkable(global_menu_name, index, true); + } + ds->global_menu_set_item_checked(global_menu_name, index, item.checked); + ds->global_menu_set_item_disabled(global_menu_name, index, item.disabled); + ds->global_menu_set_item_max_states(global_menu_name, index, item.max_states); + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + ds->global_menu_set_item_state(global_menu_name, index, item.state); + ds->global_menu_set_item_indentation_level(global_menu_name, index, item.indent); + ds->global_menu_set_item_tooltip(global_menu_name, index, item.tooltip); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } else if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + } + } + return global_menu_name; +} + +void PopupMenu::unbind_global_menu() { + if (global_menu_name.is_empty()) { + return; + } + + for (int i = 0; i < items.size(); i++) { + Item &item = items.write[i]; + if (!item.submenu.is_empty()) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(item.submenu)); + if (pm) { + pm->unbind_global_menu(); + } + item.submenu_bound = false; + } + } + DisplayServer::get_singleton()->global_menu_clear(global_menu_name); + + global_menu_name = String(); +} + String PopupMenu::_get_accel_text(const Item &p_item) const { if (p_item.shortcut.is_valid()) { return p_item.shortcut->get_as_text(); @@ -821,11 +901,17 @@ void PopupMenu::_menu_changed() { void PopupMenu::add_child_notify(Node *p_child) { Window::add_child_notify(p_child); - PopupMenu *pm = Object::cast_to<PopupMenu>(p_child); - if (!pm) { - return; + if (Object::cast_to<PopupMenu>(p_child) && !global_menu_name.is_empty()) { + String node_name = p_child->get_name(); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(node_name)); + for (int i = 0; i < items.size(); i++) { + if (items[i].submenu == node_name) { + String submenu_name = pm->bind_global_menu(); + DisplayServer::get_singleton()->global_menu_set_item_submenu(global_menu_name, i, submenu_name); + items.write[i].submenu_bound = true; + } + } } - p_child->connect("menu_changed", callable_mp(this, &PopupMenu::_menu_changed)); _menu_changed(); } @@ -836,7 +922,16 @@ void PopupMenu::remove_child_notify(Node *p_child) { if (!pm) { return; } - p_child->disconnect("menu_changed", callable_mp(this, &PopupMenu::_menu_changed)); + if (Object::cast_to<PopupMenu>(p_child) && !global_menu_name.is_empty()) { + String node_name = p_child->get_name(); + for (int i = 0; i < items.size(); i++) { + if (items[i].submenu == node_name) { + DisplayServer::get_singleton()->global_menu_set_item_submenu(global_menu_name, i, String()); + items.write[i].submenu_bound = false; + } + } + pm->unbind_global_menu(); + } _menu_changed(); } @@ -857,9 +952,15 @@ void PopupMenu::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { + DisplayServer *ds = DisplayServer::get_singleton(); + bool is_global = !global_menu_name.is_empty(); for (int i = 0; i < items.size(); i++) { - items.write[i].xl_text = atr(items[i].text); - items.write[i].dirty = true; + Item &item = items.write[i]; + item.xl_text = atr(item.text); + item.dirty = true; + if (is_global) { + ds->global_menu_set_item_text(global_menu_name, i, item.xl_text); + } _shape_item(i); } @@ -1031,6 +1132,14 @@ void PopupMenu::add_item(const String &p_label, int p_id, Key p_accel) { ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + } + _shape_item(items.size() - 1); control->queue_redraw(); @@ -1045,6 +1154,15 @@ void PopupMenu::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_labe item.icon = p_icon; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + } + _shape_item(items.size() - 1); control->queue_redraw(); @@ -1059,10 +1177,20 @@ void PopupMenu::add_check_item(const String &p_label, int p_id, Key p_accel) { item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + ds->global_menu_set_item_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1073,10 +1201,22 @@ void PopupMenu::add_icon_check_item(const Ref<Texture2D> &p_icon, const String & item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + ds->global_menu_set_item_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); + _menu_changed(); } void PopupMenu::add_radio_check_item(const String &p_label, int p_id, Key p_accel) { @@ -1085,10 +1225,20 @@ void PopupMenu::add_radio_check_item(const String &p_label, int p_id, Key p_acce item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + ds->global_menu_set_item_radio_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1099,10 +1249,21 @@ void PopupMenu::add_icon_radio_check_item(const Ref<Texture2D> &p_icon, const St item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + ds->global_menu_set_item_radio_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1113,11 +1274,22 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int item.state = p_default_state; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (item.accel != Key::NONE) { + ds->global_menu_set_item_accelerator(global_menu_name, index, item.accel); + } + ds->global_menu_set_item_max_states(global_menu_name, index, item.max_states); + ds->global_menu_set_item_state(global_menu_name, index, item.state); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); _menu_changed(); + notify_property_list_changed(); } #define ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, p_allow_echo) \ @@ -1135,10 +1307,26 @@ void PopupMenu::add_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_g ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global, p_allow_echo); items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1148,10 +1336,27 @@ void PopupMenu::add_icon_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortc item.icon = p_icon; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1161,10 +1366,27 @@ void PopupMenu::add_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bo item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } + ds->global_menu_set_item_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1175,10 +1397,28 @@ void PopupMenu::add_icon_check_shortcut(const Ref<Texture2D> &p_icon, const Ref< item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + ds->global_menu_set_item_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1188,10 +1428,27 @@ void PopupMenu::add_radio_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_ item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } + ds->global_menu_set_item_radio_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1202,10 +1459,28 @@ void PopupMenu::add_icon_radio_check_shortcut(const Ref<Texture2D> &p_icon, cons item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + if (!item.shortcut_is_disabled && item.shortcut.is_valid() && item.shortcut->has_valid_event()) { + Array events = item.shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, index, ie->get_keycode_with_modifiers()); + break; + } + } + } + ds->global_menu_set_item_icon(global_menu_name, index, item.icon); + ds->global_menu_set_item_radio_checkable(global_menu_name, index, true); + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1217,10 +1492,22 @@ void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu, item.submenu = p_submenu; items.push_back(item); + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + int index = ds->global_menu_add_item(global_menu_name, item.xl_text, callable_mp(this, &PopupMenu::activate_item), Callable(), items.size() - 1); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(item.submenu)); // Find first menu with this name. + if (pm) { + String submenu_name = pm->bind_global_menu(); + ds->global_menu_set_item_submenu(global_menu_name, index, submenu_name); + items.write[index].submenu_bound = true; + } + } + _shape_item(items.size() - 1); control->queue_redraw(); child_controls_changed(); + notify_property_list_changed(); _menu_changed(); } @@ -1240,6 +1527,10 @@ void PopupMenu::set_item_text(int p_idx, const String &p_text) { items.write[p_idx].text = p_text; items.write[p_idx].xl_text = atr(p_text); items.write[p_idx].dirty = true; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_text(global_menu_name, p_idx, items[p_idx].xl_text); + } _shape_item(p_idx); control->queue_redraw(); @@ -1284,6 +1575,10 @@ void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) { items.write[p_idx].icon = p_icon; + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_icon(global_menu_name, p_idx, items[p_idx].icon); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1332,6 +1627,10 @@ void PopupMenu::set_item_checked(int p_idx, bool p_checked) { items.write[p_idx].checked = p_checked; + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_checked(global_menu_name, p_idx, p_checked); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1349,6 +1648,10 @@ void PopupMenu::set_item_id(int p_idx, int p_id) { items.write[p_idx].id = p_id; + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_tag(global_menu_name, p_idx, p_id); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1367,6 +1670,10 @@ void PopupMenu::set_item_accelerator(int p_idx, Key p_accel) { items.write[p_idx].accel = p_accel; items.write[p_idx].dirty = true; + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_accelerator(global_menu_name, p_idx, p_accel); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1383,7 +1690,6 @@ void PopupMenu::set_item_metadata(int p_idx, const Variant &p_meta) { } items.write[p_idx].metadata = p_meta; - control->queue_redraw(); child_controls_changed(); _menu_changed(); } @@ -1399,6 +1705,11 @@ void PopupMenu::set_item_disabled(int p_idx, bool p_disabled) { } items.write[p_idx].disabled = p_disabled; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_disabled(global_menu_name, p_idx, p_disabled); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1414,7 +1725,30 @@ void PopupMenu::set_item_submenu(int p_idx, const String &p_submenu) { return; } + if (!global_menu_name.is_empty()) { + if (items[p_idx].submenu_bound) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(items[p_idx].submenu)); + if (pm) { + DisplayServer::get_singleton()->global_menu_set_item_submenu(global_menu_name, p_idx, String()); + pm->unbind_global_menu(); + } + items.write[p_idx].submenu_bound = false; + } + } + items.write[p_idx].submenu = p_submenu; + + if (!global_menu_name.is_empty()) { + if (!items[p_idx].submenu.is_empty()) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(items[p_idx].submenu)); + if (pm) { + String submenu_name = pm->bind_global_menu(); + DisplayServer::get_singleton()->global_menu_set_item_submenu(global_menu_name, p_idx, submenu_name); + items.write[p_idx].submenu_bound = true; + } + } + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1423,6 +1757,11 @@ void PopupMenu::set_item_submenu(int p_idx, const String &p_submenu) { void PopupMenu::toggle_item_checked(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].checked = !items[p_idx].checked; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_checked(global_menu_name, p_idx, items[p_idx].checked); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1569,6 +1908,11 @@ void PopupMenu::set_item_as_checkable(int p_idx, bool p_checkable) { } items.write[p_idx].checkable_type = p_checkable ? Item::CHECKABLE_TYPE_CHECK_BOX : Item::CHECKABLE_TYPE_NONE; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_checkable(global_menu_name, p_idx, p_checkable); + } + control->queue_redraw(); _menu_changed(); } @@ -1585,6 +1929,11 @@ void PopupMenu::set_item_as_radio_checkable(int p_idx, bool p_radio_checkable) { } items.write[p_idx].checkable_type = p_radio_checkable ? Item::CHECKABLE_TYPE_RADIO_BUTTON : Item::CHECKABLE_TYPE_NONE; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_radio_checkable(global_menu_name, p_idx, p_radio_checkable); + } + control->queue_redraw(); _menu_changed(); } @@ -1600,6 +1949,11 @@ void PopupMenu::set_item_tooltip(int p_idx, const String &p_tooltip) { } items.write[p_idx].tooltip = p_tooltip; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_tooltip(global_menu_name, p_idx, p_tooltip); + } + control->queue_redraw(); _menu_changed(); } @@ -1625,6 +1979,21 @@ void PopupMenu::set_item_shortcut(int p_idx, const Ref<Shortcut> &p_shortcut, bo _ref_shortcut(items[p_idx].shortcut); } + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + ds->global_menu_set_item_accelerator(global_menu_name, p_idx, Key::NONE); + if (!items[p_idx].shortcut_is_disabled && items[p_idx].shortcut.is_valid() && items[p_idx].shortcut->has_valid_event()) { + Array events = items[p_idx].shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, p_idx, ie->get_keycode_with_modifiers()); + break; + } + } + } + } + control->queue_redraw(); _menu_changed(); } @@ -1640,6 +2009,10 @@ void PopupMenu::set_item_indent(int p_idx, int p_indent) { } items.write[p_idx].indent = p_indent; + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_indentation_level(global_menu_name, p_idx, p_indent); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1656,6 +2029,11 @@ void PopupMenu::set_item_multistate(int p_idx, int p_state) { } items.write[p_idx].state = p_state; + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_state(global_menu_name, p_idx, p_state); + } + control->queue_redraw(); _menu_changed(); } @@ -1671,6 +2049,22 @@ void PopupMenu::set_item_shortcut_disabled(int p_idx, bool p_disabled) { } items.write[p_idx].shortcut_is_disabled = p_disabled; + + if (!global_menu_name.is_empty()) { + DisplayServer *ds = DisplayServer::get_singleton(); + ds->global_menu_set_item_accelerator(global_menu_name, p_idx, Key::NONE); + if (!items[p_idx].shortcut_is_disabled && items[p_idx].shortcut.is_valid() && items[p_idx].shortcut->has_valid_event()) { + Array events = items[p_idx].shortcut->get_events(); + for (int j = 0; j < events.size(); j++) { + Ref<InputEventKey> ie = events[j]; + if (ie.is_valid()) { + ds->global_menu_set_item_accelerator(global_menu_name, p_idx, ie->get_keycode_with_modifiers()); + break; + } + } + } + } + control->queue_redraw(); _menu_changed(); } @@ -1686,6 +2080,10 @@ void PopupMenu::toggle_item_multistate(int p_idx) { items.write[p_idx].state = 0; } + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_set_item_state(global_menu_name, p_idx, items[p_idx].state); + } + control->queue_redraw(); _menu_changed(); } @@ -1739,11 +2137,23 @@ void PopupMenu::set_item_count(int p_count) { return; } + DisplayServer *ds = DisplayServer::get_singleton(); + bool is_global = !global_menu_name.is_empty(); + + if (is_global && prev_size > p_count) { + for (int i = prev_size - 1; i >= p_count; i--) { + ds->global_menu_remove_item(global_menu_name, i); + } + } + items.resize(p_count); if (prev_size < p_count) { for (int i = prev_size; i < p_count; i++) { items.write[i].id = i; + if (is_global) { + ds->global_menu_add_item(global_menu_name, String(), callable_mp(this, &PopupMenu::activate_item), Callable(), i); + } } } @@ -1828,6 +2238,16 @@ bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_fo return false; } +void PopupMenu::_about_to_popup() { + ERR_MAIN_THREAD_GUARD; + emit_signal(SNAME("about_to_popup")); +} + +void PopupMenu::_about_to_close() { + ERR_MAIN_THREAD_GUARD; + emit_signal(SNAME("popup_hide")); +} + void PopupMenu::activate_item(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); ERR_FAIL_COND(items[p_idx].separator); @@ -1890,6 +2310,11 @@ void PopupMenu::remove_item(int p_idx) { } items.remove_at(p_idx); + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_remove_item(global_menu_name, p_idx); + } + control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1904,6 +2329,11 @@ void PopupMenu::add_separator(const String &p_text, int p_id) { sep.xl_text = atr(p_text); } items.push_back(sep); + + if (!global_menu_name.is_empty()) { + DisplayServer::get_singleton()->global_menu_add_separator(global_menu_name); + } + control->queue_redraw(); _menu_changed(); } @@ -1922,7 +2352,22 @@ void PopupMenu::clear(bool p_free_submenus) { } } } + + if (!global_menu_name.is_empty()) { + for (int i = 0; i < items.size(); i++) { + Item &item = items.write[i]; + if (!item.submenu.is_empty()) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(item.submenu)); + if (pm) { + pm->unbind_global_menu(); + } + item.submenu_bound = false; + } + } + DisplayServer::get_singleton()->global_menu_clear(global_menu_name); + } items.clear(); + mouse_over = -1; control->queue_redraw(); child_controls_changed(); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index f123d08400..5d5f4a8322 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -75,6 +75,7 @@ class PopupMenu : public Popup { bool shortcut_is_global = false; bool shortcut_is_disabled = false; bool allow_echo = false; + bool submenu_bound = false; // Returns (0,0) if icon is null. Size2 get_icon_size() const { @@ -88,6 +89,8 @@ class PopupMenu : public Popup { } }; + String global_menu_name; + bool close_allowed = false; bool activated_by_keyboard = false; @@ -213,6 +216,9 @@ public: virtual void _parent_focused() override; + String bind_global_menu(); + void unbind_global_menu(); + void add_item(const String &p_label, int p_id = -1, Key p_accel = Key::NONE); void add_icon_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id = -1, Key p_accel = Key::NONE); void add_check_item(const String &p_label, int p_id = -1, Key p_accel = Key::NONE); @@ -293,6 +299,9 @@ public: bool activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only = false); void activate_item(int p_idx); + void _about_to_popup(); + void _about_to_close(); + void remove_item(int p_idx); void add_separator(const String &p_text = String(), int p_id = -1); diff --git a/servers/display_server.cpp b/servers/display_server.cpp index c8516a0966..6459cc7462 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -82,6 +82,10 @@ int DisplayServer::global_menu_add_multistate_item(const String &p_menu_root, co return -1; } +void DisplayServer::global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callbacs, const Callable &p_close_callback) { + WARN_PRINT("Global menus not supported by this display server."); +} + int DisplayServer::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index) { WARN_PRINT("Global menus not supported by this display server."); return -1; @@ -106,6 +110,10 @@ void DisplayServer::global_menu_set_item_callback(const String &p_menu_root, int WARN_PRINT("Global menus not supported by this display server."); } +void DisplayServer::global_menu_set_item_hover_callbacks(const String &p_menu_root, int p_idx, const Callable &p_callback) { + WARN_PRINT("Global menus not supported by this display server."); +} + void DisplayServer::global_menu_set_item_key_callback(const String &p_menu_root, int p_idx, const Callable &p_key_callback) { WARN_PRINT("Global menus not supported by this display server."); } @@ -160,6 +168,11 @@ bool DisplayServer::global_menu_is_item_disabled(const String &p_menu_root, int return false; } +bool DisplayServer::global_menu_is_item_hidden(const String &p_menu_root, int p_idx) const { + WARN_PRINT("Global menus not supported by this display server."); + return false; +} + String DisplayServer::global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const { WARN_PRINT("Global menus not supported by this display server."); return String(); @@ -217,6 +230,10 @@ void DisplayServer::global_menu_set_item_disabled(const String &p_menu_root, int WARN_PRINT("Global menus not supported by this display server."); } +void DisplayServer::global_menu_set_item_hidden(const String &p_menu_root, int p_idx, bool p_hidden) { + WARN_PRINT("Global menus not supported by this display server."); +} + void DisplayServer::global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) { WARN_PRINT("Global menus not supported by this display server."); } @@ -581,6 +598,7 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("has_feature", "feature"), &DisplayServer::has_feature); ClassDB::bind_method(D_METHOD("get_name"), &DisplayServer::get_name); + ClassDB::bind_method(D_METHOD("global_menu_set_popup_callbacks", "menu_root", "open_callback", "close_callback"), &DisplayServer::global_menu_set_popup_callbacks); ClassDB::bind_method(D_METHOD("global_menu_add_submenu_item", "menu_root", "label", "submenu", "index"), &DisplayServer::global_menu_add_submenu_item, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("global_menu_add_item", "menu_root", "label", "callback", "key_callback", "tag", "accelerator", "index"), &DisplayServer::global_menu_add_item, DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(Variant()), DEFVAL(Key::NONE), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("global_menu_add_check_item", "menu_root", "label", "callback", "key_callback", "tag", "accelerator", "index"), &DisplayServer::global_menu_add_check_item, DEFVAL(Callable()), DEFVAL(Callable()), DEFVAL(Variant()), DEFVAL(Key::NONE), DEFVAL(-1)); @@ -604,6 +622,7 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("global_menu_get_item_submenu", "menu_root", "idx"), &DisplayServer::global_menu_get_item_submenu); ClassDB::bind_method(D_METHOD("global_menu_get_item_accelerator", "menu_root", "idx"), &DisplayServer::global_menu_get_item_accelerator); ClassDB::bind_method(D_METHOD("global_menu_is_item_disabled", "menu_root", "idx"), &DisplayServer::global_menu_is_item_disabled); + ClassDB::bind_method(D_METHOD("global_menu_is_item_hidden", "menu_root", "idx"), &DisplayServer::global_menu_is_item_hidden); ClassDB::bind_method(D_METHOD("global_menu_get_item_tooltip", "menu_root", "idx"), &DisplayServer::global_menu_get_item_tooltip); ClassDB::bind_method(D_METHOD("global_menu_get_item_state", "menu_root", "idx"), &DisplayServer::global_menu_get_item_state); ClassDB::bind_method(D_METHOD("global_menu_get_item_max_states", "menu_root", "idx"), &DisplayServer::global_menu_get_item_max_states); @@ -614,12 +633,14 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("global_menu_set_item_checkable", "menu_root", "idx", "checkable"), &DisplayServer::global_menu_set_item_checkable); ClassDB::bind_method(D_METHOD("global_menu_set_item_radio_checkable", "menu_root", "idx", "checkable"), &DisplayServer::global_menu_set_item_radio_checkable); ClassDB::bind_method(D_METHOD("global_menu_set_item_callback", "menu_root", "idx", "callback"), &DisplayServer::global_menu_set_item_callback); + ClassDB::bind_method(D_METHOD("global_menu_set_item_hover_callbacks", "menu_root", "idx", "callback"), &DisplayServer::global_menu_set_item_hover_callbacks); ClassDB::bind_method(D_METHOD("global_menu_set_item_key_callback", "menu_root", "idx", "key_callback"), &DisplayServer::global_menu_set_item_key_callback); ClassDB::bind_method(D_METHOD("global_menu_set_item_tag", "menu_root", "idx", "tag"), &DisplayServer::global_menu_set_item_tag); ClassDB::bind_method(D_METHOD("global_menu_set_item_text", "menu_root", "idx", "text"), &DisplayServer::global_menu_set_item_text); ClassDB::bind_method(D_METHOD("global_menu_set_item_submenu", "menu_root", "idx", "submenu"), &DisplayServer::global_menu_set_item_submenu); ClassDB::bind_method(D_METHOD("global_menu_set_item_accelerator", "menu_root", "idx", "keycode"), &DisplayServer::global_menu_set_item_accelerator); ClassDB::bind_method(D_METHOD("global_menu_set_item_disabled", "menu_root", "idx", "disabled"), &DisplayServer::global_menu_set_item_disabled); + ClassDB::bind_method(D_METHOD("global_menu_set_item_hidden", "menu_root", "idx", "hidden"), &DisplayServer::global_menu_set_item_hidden); ClassDB::bind_method(D_METHOD("global_menu_set_item_tooltip", "menu_root", "idx", "tooltip"), &DisplayServer::global_menu_set_item_tooltip); ClassDB::bind_method(D_METHOD("global_menu_set_item_state", "menu_root", "idx", "state"), &DisplayServer::global_menu_set_item_state); ClassDB::bind_method(D_METHOD("global_menu_set_item_max_states", "menu_root", "idx", "max_states"), &DisplayServer::global_menu_set_item_max_states); diff --git a/servers/display_server.h b/servers/display_server.h index 71bfd7b607..d2e112d224 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -130,6 +130,8 @@ public: virtual bool has_feature(Feature p_feature) const = 0; virtual String get_name() const = 0; + virtual void global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback = Callable(), const Callable &p_close_callback = Callable()); + virtual int global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1); virtual int global_menu_add_item(const String &p_menu_root, 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 global_menu_add_check_item(const String &p_menu_root, 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); @@ -153,6 +155,7 @@ public: virtual String global_menu_get_item_submenu(const String &p_menu_root, int p_idx) const; virtual Key global_menu_get_item_accelerator(const String &p_menu_root, int p_idx) const; virtual bool global_menu_is_item_disabled(const String &p_menu_root, int p_idx) const; + virtual bool global_menu_is_item_hidden(const String &p_menu_root, int p_idx) const; virtual String global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const; virtual int global_menu_get_item_state(const String &p_menu_root, int p_idx) const; virtual int global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const; @@ -164,11 +167,13 @@ public: virtual void global_menu_set_item_radio_checkable(const String &p_menu_root, int p_idx, bool p_checkable); virtual void global_menu_set_item_callback(const String &p_menu_root, int p_idx, const Callable &p_callback); virtual void global_menu_set_item_key_callback(const String &p_menu_root, int p_idx, const Callable &p_key_callback); + virtual void global_menu_set_item_hover_callbacks(const String &p_menu_root, int p_idx, const Callable &p_callback); virtual void global_menu_set_item_tag(const String &p_menu_root, int p_idx, const Variant &p_tag); virtual void global_menu_set_item_text(const String &p_menu_root, int p_idx, const String &p_text); virtual void global_menu_set_item_submenu(const String &p_menu_root, int p_idx, const String &p_submenu); virtual void global_menu_set_item_accelerator(const String &p_menu_root, int p_idx, Key p_keycode); virtual void global_menu_set_item_disabled(const String &p_menu_root, int p_idx, bool p_disabled); + virtual void global_menu_set_item_hidden(const String &p_menu_root, int p_idx, bool p_hidden); virtual void global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip); virtual void global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state); virtual void global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states); @@ -501,7 +506,8 @@ public: FILE_DIALOG_MODE_OPEN_FILES, FILE_DIALOG_MODE_OPEN_DIR, FILE_DIALOG_MODE_OPEN_ANY, - FILE_DIALOG_MODE_SAVE_FILE + FILE_DIALOG_MODE_SAVE_FILE, + FILE_DIALOG_MODE_SAVE_MAX }; virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback); diff --git a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp index 7eb8cbd02f..b9bda9329e 100644 --- a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp @@ -103,8 +103,9 @@ void RendererCompositorRD::begin_frame(double frame_step) { } void RendererCompositorRD::end_frame(bool p_swap_buffers) { - // TODO: Likely pass a bool to swap buffers to avoid display? - RD::get_singleton()->swap_buffers(); + if (p_swap_buffers) { + RD::get_singleton()->swap_buffers(); + } } void RendererCompositorRD::initialize() { diff --git a/servers/rendering/renderer_viewport.cpp b/servers/rendering/renderer_viewport.cpp index e43036f177..e8e0d2e3eb 100644 --- a/servers/rendering/renderer_viewport.cpp +++ b/servers/rendering/renderer_viewport.cpp @@ -616,7 +616,7 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { } } -void RendererViewport::draw_viewports() { +void RendererViewport::draw_viewports(bool p_swap_buffers) { timestamp_vp_map.clear(); // get our xr interface in case we need it @@ -799,11 +799,14 @@ void RendererViewport::draw_viewports() { total_draw_calls_used = draw_calls_used; RENDER_TIMESTAMP("< Render Viewports"); - //this needs to be called to make screen swapping more efficient - RSG::rasterizer->prepare_for_blitting_render_targets(); - for (const KeyValue<int, Vector<BlitToScreen>> &E : blit_to_screen_list) { - RSG::rasterizer->blit_render_targets_to_screen(E.key, E.value.ptr(), E.value.size()); + if (p_swap_buffers) { + //this needs to be called to make screen swapping more efficient + RSG::rasterizer->prepare_for_blitting_render_targets(); + + for (const KeyValue<int, Vector<BlitToScreen>> &E : blit_to_screen_list) { + RSG::rasterizer->blit_render_targets_to_screen(E.key, E.value.ptr(), E.value.size()); + } } } diff --git a/servers/rendering/renderer_viewport.h b/servers/rendering/renderer_viewport.h index 44de6d8804..a0ec9e6318 100644 --- a/servers/rendering/renderer_viewport.h +++ b/servers/rendering/renderer_viewport.h @@ -299,7 +299,7 @@ public: void handle_timestamp(String p_timestamp, uint64_t p_cpu_time, uint64_t p_gpu_time); void set_default_clear_color(const Color &p_color); - void draw_viewports(); + void draw_viewports(bool p_swap_buffers); bool free(RID p_rid); diff --git a/servers/rendering/rendering_server_default.cpp b/servers/rendering/rendering_server_default.cpp index 38fecbe323..2c8265b7d7 100644 --- a/servers/rendering/rendering_server_default.cpp +++ b/servers/rendering/rendering_server_default.cpp @@ -88,7 +88,7 @@ void RenderingServerDefault::_draw(bool p_swap_buffers, double frame_step) { RSG::scene->render_probes(); - RSG::viewport->draw_viewports(); + RSG::viewport->draw_viewports(p_swap_buffers); RSG::canvas_render->update(); if (!OS::get_singleton()->get_current_rendering_driver_name().begins_with("opengl3")) { |