diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cc6a211f17d..dd62fe8872c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -141,6 +141,7 @@ repos: exclude: | (?x)^( core/math/bvh_.*\.inc$| + editor/plugins/gdextension/cpp_scons/template/*| platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*| platform/android/java/editor/src/main/java/com/android/.*| platform/android/java/lib/src/com/.*| diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index c6144a34cb69..31a89e99f6ea 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -141,7 +141,7 @@ #include "editor/plugins/editor_plugin.h" #include "editor/plugins/editor_preview_plugins.h" #include "editor/plugins/editor_resource_conversion_plugin.h" -#include "editor/plugins/gdextension_export_plugin.h" +#include "editor/plugins/gdextension/gdextension_export_plugin.h" #include "editor/plugins/material_editor_plugin.h" #include "editor/plugins/mesh_library_editor_plugin.h" #include "editor/plugins/node_3d_editor_plugin.h" diff --git a/editor/gui/editor_validation_panel.cpp b/editor/gui/editor_validation_panel.cpp index a4ca743bd786..b2d92c35a8ea 100644 --- a/editor/gui/editor_validation_panel.cpp +++ b/editor/gui/editor_validation_panel.cpp @@ -64,10 +64,10 @@ void EditorValidationPanel::add_line(int p_id, const String &p_valid_message) { ERR_FAIL_COND(valid_messages.has(p_id)); Label *label = memnew(Label); - message_container->add_child(label); label->set_custom_minimum_size(Size2(200 * EDSCALE, 0)); label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); + message_container->add_child(label); valid_messages[p_id] = p_valid_message; labels[p_id] = label; @@ -124,6 +124,10 @@ void EditorValidationPanel::set_message(int p_id, const String &p_text, MessageT } } +int EditorValidationPanel::get_message_count() const { + return valid_messages.size(); +} + bool EditorValidationPanel::is_valid() const { return valid; } diff --git a/editor/gui/editor_validation_panel.h b/editor/gui/editor_validation_panel.h index c371795e150d..b35a8b48b9d6 100644 --- a/editor/gui/editor_validation_panel.h +++ b/editor/gui/editor_validation_panel.h @@ -80,6 +80,7 @@ class EditorValidationPanel : public PanelContainer { void update(); void set_message(int p_id, const String &p_text, MessageType p_type, bool p_auto_prefix = true); + int get_message_count() const; bool is_valid() const; EditorValidationPanel(); diff --git a/editor/plugins/SCsub b/editor/plugins/SCsub index 4b6abf18f946..1e6dc012d410 100644 --- a/editor/plugins/SCsub +++ b/editor/plugins/SCsub @@ -4,5 +4,6 @@ Import("env") env.add_source_files(env.editor_sources, "*.cpp") +SConscript("gdextension/SCsub") SConscript("gizmos/SCsub") SConscript("tiles/SCsub") diff --git a/editor/plugins/gdextension/SCsub b/editor/plugins/gdextension/SCsub new file mode 100644 index 000000000000..9907b99b28b2 --- /dev/null +++ b/editor/plugins/gdextension/SCsub @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.editor_sources, "*.cpp") + +SConscript("cpp_scons/SCsub") diff --git a/editor/plugins/gdextension/cpp_scons/SCsub b/editor/plugins/gdextension/cpp_scons/SCsub new file mode 100644 index 000000000000..ff01caa91ac9 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/SCsub @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import os + +Import("env") + +env.add_source_files(env.editor_sources, "*.cpp") + + +def parse_template(source): + with open(source) as file: + lines = file.readlines() + script_template = "" + for line in lines: + script_template += line + if env["precision"] != "double": + script_template = script_template.replace('ARGUMENTS["precision"] = "double"', "") + name = os.path.basename(source).upper().replace(".", "_") + return "\nconst String " + name + ' = R"(' + script_template.rstrip() + ')";\n' + + +def make_templates(target, source, env): + dst = str(target[0]) + with StringIO() as s: + s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n\n") + s.write("#ifndef GDEXTENSION_TEMPLATE_FILES_GEN_H\n") + s.write("#define GDEXTENSION_TEMPLATE_FILES_GEN_H\n\n") + s.write('#include "core/string/ustring.h"\n') + parsed_template_string = "" + for file in source: + filepath = str(file) + if os.path.isfile(filepath): + parsed_template_string += parse_template(filepath) + s.write(parsed_template_string) + s.write("\n#endif // GDEXTENSION_TEMPLATE_FILES_GEN_H\n") + with open(dst, "w", encoding="utf-8", newline="\n") as f: + f.write(s.getvalue()) + + +env["BUILDERS"]["MakeGDExtTemplateBuilder"] = Builder( + action=env.Run(make_templates), + suffix=".h", +) + +# Template files +templates_sources = Glob("template/*") + Glob("template/*/*") + Glob("template/*/*/*") + +dest_file = "gdextension_template_files.gen.h" +env.Alias("editor_template_gdext", [env.MakeGDExtTemplateBuilder(dest_file, templates_sources)]) diff --git a/editor/plugins/gdextension/cpp_scons/cpp_scons_gdext_creator.cpp b/editor/plugins/gdextension/cpp_scons/cpp_scons_gdext_creator.cpp new file mode 100644 index 000000000000..cba049222d2f --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/cpp_scons_gdext_creator.cpp @@ -0,0 +1,255 @@ +/**************************************************************************/ +/* cpp_scons_gdext_creator.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "cpp_scons_gdext_creator.h" + +#include "core/core_bind.h" +#include "core/io/dir_access.h" +#include "core/string/string_builder.h" +#include "core/version.h" +#include "gdextension_template_files.gen.h" + +#include "editor/editor_node.h" + +void CppSconsGDExtensionCreator::_git_clone_godot_cpp(const String &p_parent_path, bool p_compile) { + EditorProgress ep("Preparing GDExtension C++ plugin", "Preparing GDExtension C++ plugin", 3); + List args; + args.push_back("clone"); + args.push_back("--single-branch"); + args.push_back("--branch"); + args.push_back(VERSION_BRANCH); + args.push_back("https://github.com/godotengine/godot-cpp"); + const String godot_cpp_path = p_parent_path.trim_prefix("res://").path_join("godot-cpp"); + args.push_back(godot_cpp_path); + ep.step(TTR("Cloning godot-cpp..."), 1); + String output = ""; + int result = OS::get_singleton()->execute("git", args, &output); + Ref dir = DirAccess::create(DirAccess::ACCESS_RESOURCES); + if (result != 0 || !dir->dir_exists(godot_cpp_path)) { + args.get(3) = "master"; + output = ""; + result = OS::get_singleton()->execute("git", args, &output); + } + ERR_FAIL_COND_MSG(result != 0 || !dir->dir_exists(godot_cpp_path), "Failed to clone godot-cpp. Please clone godot-cpp manually in order to have a working GDExtension plugin."); + if (p_compile) { + ep.step(TTR("Performing initial compile... (this may take several minutes)"), 2); + result = OS::get_singleton()->execute("scons", List()); + ERR_FAIL_COND_MSG(result != 0, "Failed to compile godot-cpp. Please ensure SCons is installed, then run the `scons` command in your project."); + } + ep.step(TTR("Done!"), 3); +} + +String CppSconsGDExtensionCreator::_process_template(const String &p_contents) { + String ret; + if (strip_module_defines) { + StringBuilder builder; + bool keep = true; + PackedStringArray lines = p_contents.split("\n"); + for (const String &line : lines) { + if (line == "#if GDEXTENSION" || line == "#else") { + continue; + } else if (line == "#elif GODOT_MODULE") { + keep = false; + continue; + } else if (line == "#endif") { + keep = true; + continue; + } + if (keep) { + builder += line; + builder += "\n"; + } + } + ret = builder.as_string(); + } else { + ret = p_contents; + } + if (ClassDB::class_exists("ExampleNode")) { + ret = ret.replace("ExampleNode", example_node_name); + } + ret = ret.replace("__BASE_NAME__", base_name); + ret = ret.replace("__BASE_NAME_UPPER__", base_name.to_upper()); + ret = ret.replace("__LIBRARY_NAME__", library_name); + ret = ret.replace("__LIBRARY_NAME_UPPER__", library_name.to_upper()); + ret = ret.replace("__GODOT_VERSION__", VERSION_BRANCH); + ret = ret.replace("__BASE_PATH__", res_path.trim_prefix("res://")); + ret = ret.replace("__UPDIR_DOTS__", updir_dots); + return ret; +} + +void CppSconsGDExtensionCreator::_write_file(const String &p_file_path, const String &p_contents) { + Error err; + Ref file = FileAccess::open(p_file_path, FileAccess::WRITE, &err); + ERR_FAIL_COND_MSG(err != OK, "Couldn't write file at path: " + p_file_path + "."); + file->store_string(_process_template(p_contents)); + file->close(); +} + +void CppSconsGDExtensionCreator::_ensure_file_contains(const String &p_file_path, const String &p_new_contents) { + Error err; + Ref file = FileAccess::open(p_file_path, FileAccess::READ_WRITE, &err); + if (err != OK) { + _write_file(p_file_path, p_new_contents); + return; + } + String new_contents = _process_template(p_new_contents); + String existing_contents = file->get_as_text(); + if (existing_contents.is_empty()) { + file->store_string(new_contents); + } else { + file->seek_end(); + PackedStringArray lines = new_contents.split("\n", false); + for (const String &line : lines) { + if (!existing_contents.contains(line)) { + file->store_string(line + "\n"); + } + } + } + file->close(); +} + +void CppSconsGDExtensionCreator::_write_common_files_and_dirs() { + DirAccess::make_dir_recursive_absolute(res_path.path_join("doc_classes")); + DirAccess::make_dir_recursive_absolute(res_path.path_join("icons")); + DirAccess::make_dir_recursive_absolute(res_path.path_join("src")); + _ensure_file_contains("res://SConstruct", SCONSTRUCT_TOP_LEVEL); + _write_file(res_path.path_join("doc_classes/" + example_node_name + ".xml"), EXAMPLENODE_XML); + _write_file(res_path.path_join("icons/" + example_node_name + ".svg"), EXAMPLENODE_SVG); + _write_file(res_path.path_join("icons/" + example_node_name + ".svg.import"), EXAMPLENODE_SVG_IMPORT); + _write_file(res_path.path_join("src/.gdignore"), ""); + _write_file(res_path.path_join(".gitignore"), GDEXT_GITIGNORE + "\n*.obj"); + _write_file(res_path.path_join(library_name + ".gdextension"), LIBRARY_NAME_GDEXTENSION); +} + +void CppSconsGDExtensionCreator::_write_gdext_only_files() { + _ensure_file_contains("res://.gitignore", "*.dblite"); + _write_file(res_path.path_join("src/example_node.cpp"), EXAMPLE_NODE_CPP); + _write_file(res_path.path_join("src/example_node.h"), EXAMPLE_NODE_H); + _write_file(res_path.path_join("src/register_types.cpp"), REGISTER_TYPES_CPP); + _write_file(res_path.path_join("src/register_types.h"), REGISTER_TYPES_H); + _write_file(res_path.path_join("src/" + library_name + "_defines.h"), GDEXT_DEFINES_H); + _write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP.replace("#include \"__UPDIR_DOTS__/../", "#include \"")); + _write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON.replace(" + Glob(\"__UPDIR_DOTS__/*.cpp\")", "").replace(",__UPDIR_DOTS__/", "")); +} + +void CppSconsGDExtensionCreator::_write_gdext_module_files() { + _ensure_file_contains("res://.gitignore", GDEXT_GITIGNORE); + DirAccess::make_dir_recursive_absolute("res://tests"); + _write_file("res://SCsub", SCSUB); + _write_file("res://config.py", CONFIG_PY); + _write_file("res://example_node.cpp", EXAMPLE_NODE_CPP); + _write_file("res://example_node.h", EXAMPLE_NODE_H); + _write_file("res://register_types.cpp", REGISTER_TYPES_CPP); + _write_file("res://register_types.h", REGISTER_TYPES_H); + _write_file("res://" + library_name + "_defines.h", SHARED_DEFINES_H); + _write_file("res://tests/test_" + base_name + ".h", TEST_BASE_NAME_H); + _write_file("res://tests/test_example_node.h", TEST_EXAMPLE_NODE_H); + _write_file(res_path.path_join("src/initialize_gdextension.cpp"), INITIALIZE_GDEXTENSION_CPP); + _write_file(res_path.path_join("SConstruct"), SCONSTRUCT_ADDON); +} + +void CppSconsGDExtensionCreator::create_gdextension(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) { + res_path = p_path; + base_name = p_base_name; + library_name = p_library_name; + updir_dots = String("../").repeat(p_path.count("/", 6)) + ".."; + strip_module_defines = p_variation_index == LANG_VAR_GDEXT_ONLY; + if (ClassDB::class_exists("ExampleNode")) { + int discriminator = 2; + example_node_name = "ExampleNode2"; + while (ClassDB::class_exists(example_node_name)) { + discriminator++; + example_node_name = "ExampleNode" + itos(discriminator); + } + } + _write_common_files_and_dirs(); + if (p_variation_index == LANG_VAR_GDEXT_ONLY) { + _write_gdext_only_files(); + } else { + _write_gdext_module_files(); + } + if (does_git_exist) { + _git_clone_godot_cpp(p_path.path_join("src"), p_compile); + } +} + +void CppSconsGDExtensionCreator::setup_creator() { + // Check for Git and SCons. + List args; + args.push_back("--version"); + String output; + OS::get_singleton()->execute("git", args, &output); + if (output.is_empty()) { + does_git_exist = false; + } else { + does_git_exist = true; + output = ""; + OS::get_singleton()->execute("scons", args, &output); + does_scons_exist = !output.is_empty(); + } +} + +PackedStringArray CppSconsGDExtensionCreator::get_language_variations() const { + PackedStringArray variants; + // Keep this in sync with enum LanguageVariation. + variants.push_back("C++ with SCons, GDExtension only"); + variants.push_back("C++ with SCons, GDExtension and engine module"); + return variants; +} + +Dictionary CppSconsGDExtensionCreator::get_validation_messages(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) { + Dictionary messages; + // Check for Git and SCons. + MessageType compile_consequence = p_compile ? MSG_ERROR : MSG_WARNING; + if (does_git_exist) { + if (does_scons_exist) { +#ifdef WINDOWS_ENABLED + messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC, Clang/LLVM, or MSVC from Visual Studio.")] = MSG_OK; +#else + messages[TTR("Both Git and SCons were found. You also need a C++17-compatible compiler, such as GCC or Clang/LLVM.")] = MSG_OK; +#endif + } else { + messages[TTR("Cannot compile now, SCons was not found.")] = compile_consequence; + } + } else { + messages[TTR("Cannot compile now, Git was not found.")] = compile_consequence; + } + // Check for existing engine module. + if (p_variation_index == LANG_VAR_GDEXT_MODULE) { + Ref dir = DirAccess::create(DirAccess::ACCESS_RESOURCES); + if (dir->file_exists("SCsub")) { + messages[TTR("This project already contains a C++ engine module.")] = MSG_ERROR; + } else { + messages[TTR("Able to create engine module in this Godot project.")] = MSG_OK; + messages[TTR("Warning: This will turn the root of your project into an engine module!")] = MSG_WARNING; + } + } + return messages; +} diff --git a/editor/plugins/gdextension/cpp_scons/cpp_scons_gdext_creator.h b/editor/plugins/gdextension/cpp_scons/cpp_scons_gdext_creator.h new file mode 100644 index 000000000000..2bfec368ac24 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/cpp_scons_gdext_creator.h @@ -0,0 +1,69 @@ +/**************************************************************************/ +/* cpp_scons_gdext_creator.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef CPP_SCONS_GDEXT_CREATOR_H +#define CPP_SCONS_GDEXT_CREATOR_H + +#include "../gdextension_creator_plugin.h" + +class CppSconsGDExtensionCreator : public GDExtensionCreatorPlugin { + // Keep this in sync with get_language_variations(). + enum LanguageVariation { + LANG_VAR_GDEXT_ONLY, + LANG_VAR_GDEXT_MODULE, + }; + + // Used by _process_template. + bool strip_module_defines = false; + String base_name; + String library_name; + String example_node_name = "ExampleNode"; + String res_path; + String updir_dots; + + bool does_git_exist = false; + bool does_scons_exist = false; + + void _git_clone_godot_cpp(const String &p_parent_path, bool p_compile); + String _process_template(const String &p_contents); + void _write_common_files_and_dirs(); + void _write_gdext_only_files(); + void _write_gdext_module_files(); + void _write_file(const String &p_file_path, const String &p_contents); + void _ensure_file_contains(const String &p_file_path, const String &p_new_contents); + +public: + void create_gdextension(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) override; + void setup_creator() override; + PackedStringArray get_language_variations() const override; + Dictionary get_validation_messages(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) override; +}; + +#endif // CPP_SCONS_GDEXT_CREATOR_H diff --git a/editor/plugins/gdextension/cpp_scons/template/SConstruct_top_level b/editor/plugins/gdextension/cpp_scons/template/SConstruct_top_level new file mode 100644 index 000000000000..ff52b70b8bb7 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/SConstruct_top_level @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +# This file is for building as a Godot GDExtension. +SConscript("__BASE_PATH__/SConstruct") diff --git a/editor/plugins/gdextension/cpp_scons/template/SCsub b/editor/plugins/gdextension/cpp_scons/template/SCsub new file mode 100644 index 000000000000..f21479c64dd9 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/SCsub @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +# This file is for building as a Godot engine module. + +Import("env") +Import("env_modules") + +env_modules.Append(CPPDEFINES=["GODOT_MODULE"]) + +env___BASE_NAME__ = env_modules.Clone() + +env___BASE_NAME__.add_source_files(env.modules_sources, "*.cpp") + +if env.editor_build: + # If your module has editor-specific code, you can add it here. + # env___BASE_NAME__.add_source_files(env.modules_sources, "editor/*.cpp") + pass diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/SConstruct_addon b/editor/plugins/gdextension/cpp_scons/template/addon/SConstruct_addon new file mode 100644 index 000000000000..95f4dd7ad3a1 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/SConstruct_addon @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +# This file is for building as a Godot GDExtension. +ARGUMENTS["precision"] = "double" +env = SConscript("src/godot-cpp/SConstruct") + +# Add source files. +env.Append(CPPPATH=["./,src/,__UPDIR_DOTS__/"]) +sources = Glob("*.cpp") + Glob("src/*.cpp") + Glob("__UPDIR_DOTS__/*.cpp") + +env.Append(CPPDEFINES=["GDEXTENSION"]) + +bin_path = "bin/" +extension_name = "__LIBRARY_NAME__" +debug_or_release = "release" if env["target"] == "template_release" else "debug" + +if not "arch_suffix" in env: + env["arch_suffix"] = env["arch"] + +if env["target"] in ["editor", "template_debug"]: + try: + doc_data = env.GodotCPPDocData("src/gen/doc_data.gen.cpp", source=Glob("doc_classes/*.xml")) + sources.append(doc_data) + except AttributeError: + print("Not including class reference as we're targeting a pre-4.3 baseline.") + +# Create the library target (e.g. lib__LIBRARY_NAME__.linux.debug.x86_64.so). +if env["platform"] == "macos": + library = env.SharedLibrary( + "{0}/lib{1}.{2}.{3}.framework/{1}.{2}.{3}".format( + bin_path, + extension_name, + env["platform"], + debug_or_release, + ), + source=sources, + ) +else: + library = env.SharedLibrary( + "{}/lib{}.{}.{}.{}{}".format( + bin_path, + extension_name, + env["platform"], + debug_or_release, + env["arch_suffix"], + env["SHLIBSUFFIX"], + ), + source=sources, + ) + +Default(library) diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/doc_classes/ExampleNode.xml b/editor/plugins/gdextension/cpp_scons/template/addon/doc_classes/ExampleNode.xml new file mode 100644 index 000000000000..6d59a76dd208 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/doc_classes/ExampleNode.xml @@ -0,0 +1,19 @@ + + + + GDExtension example node. + + + An example node that demonstrates how to create a node in GDExtension. This example node can also be compiled as a module. + + + + + + + + Returns either "Hello, World! From GDExtension." if compiled as GDExtension or "Hello, World! From a module." if compiled as a module. + + + + diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/gdext.gitignore b/editor/plugins/gdextension/cpp_scons/template/addon/gdext.gitignore new file mode 100644 index 000000000000..df30f370dd73 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/gdext.gitignore @@ -0,0 +1,10 @@ +bin/ +*.gen.* +*.o +*.os +*.so +*.bc +*.pyc +*.dblite +*.pdb +*.lib diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/icons/ExampleNode.svg b/editor/plugins/gdextension/cpp_scons/template/addon/icons/ExampleNode.svg new file mode 100644 index 000000000000..885048393985 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/icons/ExampleNode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/icons/ExampleNode.svg.import b/editor/plugins/gdextension/cpp_scons/template/addon/icons/ExampleNode.svg.import new file mode 100644 index 000000000000..9a86152c2866 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/icons/ExampleNode.svg.import @@ -0,0 +1,3 @@ +[params] + +svg/scale=4.0 diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/library_name.gdextension b/editor/plugins/gdextension/cpp_scons/template/addon/library_name.gdextension new file mode 100644 index 000000000000..90fc4a534bae --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/library_name.gdextension @@ -0,0 +1,28 @@ +[configuration] + +entry_symbol = "__LIBRARY_NAME___library_init" +compatibility_minimum = __GODOT_VERSION__ +reloadable = true + +[libraries] + +macos.debug = "bin/lib__LIBRARY_NAME__.macos.debug.framework" +macos.release = "bin/lib__LIBRARY_NAME__.macos.release.framework" +windows.debug.x86_64 = "bin/lib__LIBRARY_NAME__.windows.debug.x86_64.dll" +windows.release.x86_64 = "bin/lib__LIBRARY_NAME__.windows.release.x86_64.dll" +linux.debug.x86_64 = "bin/lib__LIBRARY_NAME__.linux.debug.x86_64.so" +linux.release.x86_64 = "bin/lib__LIBRARY_NAME__.linux.release.x86_64.so" +linux.debug.arm64 = "bin/lib__LIBRARY_NAME__.linux.debug.arm64.so" +linux.release.arm64 = "bin/lib__LIBRARY_NAME__.linux.release.arm64.so" +linux.debug.rv64 = "bin/lib__LIBRARY_NAME__.linux.debug.rv64.so" +linux.release.rv64 = "bin/lib__LIBRARY_NAME__.linux.release.rv64.so" +web.debug.wasm32 = "bin/lib__LIBRARY_NAME__.web.debug.wasm32.wasm" +web.release.wasm32 = "bin/lib__LIBRARY_NAME__.web.release.wasm32.wasm" +android.debug.arm64 = "bin/lib__LIBRARY_NAME__.android.debug.arm64.so" +android.release.arm64 = "bin/lib__LIBRARY_NAME__.android.release.arm64.so" +ios.debug = "bin/lib__LIBRARY_NAME__.ios.debug.universal.dylib" +ios.release = "bin/lib__LIBRARY_NAME__.ios.release.universal.dylib" + +[icons] + +ExampleNode = "icons/ExampleNode.svg" diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/src/gdext_defines.h b/editor/plugins/gdextension/cpp_scons/template/addon/src/gdext_defines.h new file mode 100644 index 000000000000..f98c61a894f0 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/src/gdext_defines.h @@ -0,0 +1,16 @@ +#ifndef __LIBRARY_NAME_UPPER___DEFINES_H +#define __LIBRARY_NAME_UPPER___DEFINES_H + +// This file should be included before any other files. + +// The build system already defines GDEXTENSION, but this helps IDEs detect the build mode. +#define GDEXTENSION 1 + +// Extremely common classes used by most files. Customize for your extension as needed. +#include +#include +// Including the namespace helps make GDExtension code more similar to module code. +// Remove this if you prefer to use the `godot::` namespace explicitly. +using namespace godot; + +#endif // __LIBRARY_NAME___DEFINES_H diff --git a/editor/plugins/gdextension/cpp_scons/template/addon/src/initialize_gdextension.cpp b/editor/plugins/gdextension/cpp_scons/template/addon/src/initialize_gdextension.cpp new file mode 100644 index 000000000000..7ee2048c0421 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/addon/src/initialize_gdextension.cpp @@ -0,0 +1,16 @@ +#include "__UPDIR_DOTS__/../register_types.h" + +#include + +extern "C" { +// Initialization. +GDExtensionBool GDE_EXPORT __LIBRARY_NAME___library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { + godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + + init_obj.register_initializer(initialize___BASE_NAME___module); + init_obj.register_terminator(uninitialize___BASE_NAME___module); + init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + + return init_obj.init(); +} +} // extern "C" diff --git a/editor/plugins/gdextension/cpp_scons/template/config.py b/editor/plugins/gdextension/cpp_scons/template/config.py new file mode 100644 index 000000000000..9cfa0a03e813 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/config.py @@ -0,0 +1,21 @@ +# This file is for building as a Godot module. +def can_build(env, platform): + return True + + +def configure(env): + pass + + +def get_doc_classes(): + return [ + "ExampleNode", + ] + + +def get_doc_path(): + return "__BASE_PATH__/doc_classes" + + +def get_icons_path(): + return "__BASE_PATH__/icons" diff --git a/editor/plugins/gdextension/cpp_scons/template/example_node.cpp b/editor/plugins/gdextension/cpp_scons/template/example_node.cpp new file mode 100644 index 000000000000..1a79ffee0714 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/example_node.cpp @@ -0,0 +1,13 @@ +#include "example_node.h" + +String ExampleNode::hello() const { +#if GDEXTENSION + return "Hello, World! From GDExtension."; +#elif GODOT_MODULE + return "Hello, World! From a module."; +#endif +} + +void ExampleNode::_bind_methods() { + ClassDB::bind_method(D_METHOD("hello"), &ExampleNode::hello); +} diff --git a/editor/plugins/gdextension/cpp_scons/template/example_node.h b/editor/plugins/gdextension/cpp_scons/template/example_node.h new file mode 100644 index 000000000000..5d226abbd2b7 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/example_node.h @@ -0,0 +1,22 @@ +#ifndef EXAMPLE_NODE_H +#define EXAMPLE_NODE_H + +#include "__LIBRARY_NAME___defines.h" + +#if GDEXTENSION +#include +#elif GODOT_MODULE +#include "scene/main/node.h" +#endif + +class ExampleNode : public Node { + GDCLASS(ExampleNode, Node); + +protected: + static void _bind_methods(); + +public: + String hello() const; +}; + +#endif // EXAMPLE_NODE_H diff --git a/editor/plugins/gdextension/cpp_scons/template/register_types.cpp b/editor/plugins/gdextension/cpp_scons/template/register_types.cpp new file mode 100644 index 000000000000..39a847eaac83 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/register_types.cpp @@ -0,0 +1,40 @@ +#include "register_types.h" + +#if GDEXTENSION +#include +#elif GODOT_MODULE +#include "core/config/engine.h" +#endif + +#include "example_node.h" + +inline void add_godot_singleton(const StringName &p_singleton_name, Object *p_object) { +#if GDEXTENSION + Engine::get_singleton()->register_singleton(p_singleton_name, p_object); +#elif GODOT_MODULE + Engine::get_singleton()->add_singleton(Engine::Singleton(p_singleton_name, p_object)); +#endif +} + +inline void remove_godot_singleton(const StringName &p_singleton_name) { +#if GDEXTENSION + Engine::get_singleton()->unregister_singleton(p_singleton_name); +#elif GODOT_MODULE + Engine::get_singleton()->remove_singleton(p_singleton_name); +#endif +} + +void initialize___BASE_NAME___module(ModuleInitializationLevel p_level) { + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + // Register node classes here. + // You can add singletons using add_godot_singleton(). + GDREGISTER_CLASS(ExampleNode); + } +} + +void uninitialize___BASE_NAME___module(ModuleInitializationLevel p_level) { + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + // Perform cleanup here. + // You can remove singletons using remove_godot_singleton(). + } +} diff --git a/editor/plugins/gdextension/cpp_scons/template/register_types.h b/editor/plugins/gdextension/cpp_scons/template/register_types.h new file mode 100644 index 000000000000..f17736bea609 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/register_types.h @@ -0,0 +1,17 @@ +#ifndef REGISTER_TYPES_H +#define REGISTER_TYPES_H + +#include "__LIBRARY_NAME___defines.h" + +#if GDEXTENSION +#include +#elif GODOT_MODULE +#include "modules/register_module_types.h" +#else +#error "Must build as Godot GDExtension or Godot module." +#endif + +void initialize___BASE_NAME___module(ModuleInitializationLevel p_level); +void uninitialize___BASE_NAME___module(ModuleInitializationLevel p_level); + +#endif // REGISTER_TYPES_H diff --git a/editor/plugins/gdextension/cpp_scons/template/shared_defines.h b/editor/plugins/gdextension/cpp_scons/template/shared_defines.h new file mode 100644 index 000000000000..714c8586e95e --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/shared_defines.h @@ -0,0 +1,26 @@ +#ifndef __LIBRARY_NAME_UPPER___DEFINES_H +#define __LIBRARY_NAME_UPPER___DEFINES_H +// This file should be included before any other files. + +// Uncomment one of these to help IDEs detect the build mode. +// The build system already defines one of these, so keep them +// commented out when committing. +#ifndef GDEXTENSION +//#define GDEXTENSION 1 +#endif // GDEXTENSION + +#ifndef GODOT_MODULE +//#define GODOT_MODULE 1 +#endif // GODOT_MODULE + +#if GDEXTENSION +#include +#include +// Including the namespace helps make GDExtension code more similar to module code. +using namespace godot; +#elif GODOT_MODULE +#include "core/object/class_db.h" +#include "core/string/ustring.h" +#endif + +#endif // __LIBRARY_NAME_UPPER___DEFINES_H diff --git a/editor/plugins/gdextension/cpp_scons/template/tests/test_base_name.h b/editor/plugins/gdextension/cpp_scons/template/tests/test_base_name.h new file mode 100644 index 000000000000..8e772621634d --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/tests/test_base_name.h @@ -0,0 +1,6 @@ +#ifndef TEST___BASE_NAME_UPPER___H +#define TEST___BASE_NAME_UPPER___H + +#include "test_example_node.h" + +#endif // TEST___BASE_NAME_UPPER___H diff --git a/editor/plugins/gdextension/cpp_scons/template/tests/test_example_node.h b/editor/plugins/gdextension/cpp_scons/template/tests/test_example_node.h new file mode 100644 index 000000000000..8e7ee0cca589 --- /dev/null +++ b/editor/plugins/gdextension/cpp_scons/template/tests/test_example_node.h @@ -0,0 +1,15 @@ +#ifndef TEST_EXAMPLE_NODE_H +#define TEST_EXAMPLE_NODE_H + +#include "../example_node.h" +#include "tests/test_macros.h" + +namespace TestExampleNode { +TEST_CASE("[ExampleNode] Hello") { + ExampleNode test = ExampleNode(); + const String hello_text = test.hello(); + REQUIRE(hello_text == "Hello, World! From a module."); +} +} // namespace TestExampleNode + +#endif // TEST_EXAMPLE_NODE_H diff --git a/editor/plugins/gdextension/gdextension_create_dialog.cpp b/editor/plugins/gdextension/gdextension_create_dialog.cpp new file mode 100644 index 000000000000..110cb26adc11 --- /dev/null +++ b/editor/plugins/gdextension/gdextension_create_dialog.cpp @@ -0,0 +1,265 @@ +/**************************************************************************/ +/* gdextension_create_dialog.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdextension_create_dialog.h" + +#include "gdextension_creator_plugin.h" + +#include "core/io/dir_access.h" +#include "editor/gui/editor_validation_panel.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/check_box.h" +#include "scene/gui/grid_container.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/option_button.h" + +void GDExtensionCreateDialog::load_plugin_creators(const Vector> &p_plugin_creators) { + plugin_creators = p_plugin_creators; + language_option->clear(); + language_option_index_map.clear(); + for (int i = 0; i < plugin_creators.size(); i++) { + const Ref plugin_creator = plugin_creators[i]; + plugin_creator->setup_creator(); + const Vector lang_variations = plugin_creator->get_language_variations(); + for (int j = 0; j < lang_variations.size(); j++) { + language_option->add_item(lang_variations[j]); + language_option_index_map.push_back(Vector2i(i, j)); + } + } + language_option->select(0); +} + +void GDExtensionCreateDialog::_clear_fields() { + base_name_edit->clear(); + library_name_edit->clear(); + path_edit->clear(); + library_name_edit->set_placeholder("my_extension"); + path_edit->set_placeholder("res://addons/my_extension"); +} + +void GDExtensionCreateDialog::_on_canceled() { + _clear_fields(); +} + +void GDExtensionCreateDialog::_on_confirmed() { + const String valid_base_name = _get_valid_base_name(); + const String valid_library_name = _get_valid_library_name(valid_base_name); + const String valid_path = _get_valid_path(valid_base_name); + Ref dir = DirAccess::create(DirAccess::ACCESS_RESOURCES); + dir->make_dir_recursive(valid_path); + Vector2i selected_indices = language_option_index_map[language_option->get_selected_id()]; + Ref plugin_creator = plugin_creators[selected_indices.x]; + plugin_creator->create_gdextension(valid_path, valid_base_name, valid_library_name, selected_indices.y, compile_checkbox->is_pressed()); + _clear_fields(); + emit_signal("gdextension_created"); +} + +void GDExtensionCreateDialog::_on_required_text_changed() { + // Erase existing messages. + const int start_message_count = validation_panel->get_message_count(); + for (int i = 1; i < start_message_count; i++) { + validation_panel->set_message(i, "", EditorValidationPanel::MSG_INFO); + } + if (base_name_edit->get_text().is_empty()) { + validation_panel->set_message(MSG_ID_BASE_NAME, TTR("Please specify a base name."), EditorValidationPanel::MSG_ERROR); + library_name_edit->set_placeholder("my_extension"); + path_edit->set_placeholder("res://addons/my_extension"); + return; + } + // Update included messages for the text boxes. + const String valid_base_name = _get_valid_base_name(); + const String valid_library_name = _get_valid_library_name(valid_base_name); + const String valid_path = _get_valid_path(valid_base_name); + library_name_edit->set_placeholder(valid_library_name); + path_edit->set_placeholder(valid_path); + validation_panel->set_message(MSG_ID_BASE_NAME, TTR("Base name will be: ") + valid_base_name, EditorValidationPanel::MSG_OK); + validation_panel->set_message(MSG_ID_LIBRARY_NAME, TTR("Library name will be: ") + valid_library_name, EditorValidationPanel::MSG_OK); + validation_panel->set_message(MSG_ID_PATH, TTR("Path will be: ") + valid_path, EditorValidationPanel::MSG_OK); + // Write new custom messages. + Vector2i selected_indices = language_option_index_map[language_option->get_selected_id()]; + Ref plugin_creator = plugin_creators[selected_indices.x]; + Dictionary validation_messages = plugin_creator->get_validation_messages(valid_base_name, valid_library_name, valid_path, selected_indices.y, compile_checkbox->is_pressed()); + Array messages = validation_messages.keys(); + for (int i = 0; i < messages.size(); i++) { + const String message = messages[i]; + const int status = validation_messages[message]; + const int index = i + MSG_ID_MAX; + if (index >= start_message_count) { + validation_panel->add_line(index, ""); + } + validation_panel->set_message(index, message, (EditorValidationPanel::MessageType)status); + } +} + +String GDExtensionCreateDialog::_get_valid_base_name() { + String text = base_name_edit->get_text().strip_edges().validate_ascii_identifier(); + if (text.begins_with("_")) { + return text.substr(1); + } + return text; +} + +String GDExtensionCreateDialog::_get_valid_library_name(const String &p_valid_base_name) { + String text = library_name_edit->get_text().strip_edges(); + if (text.is_empty()) { + text = p_valid_base_name; + } + if (is_digit(text[0])) { + text = "godot_" + text; + } + return text.validate_ascii_identifier(); +} + +String GDExtensionCreateDialog::_get_valid_path(const String &p_valid_base_name) { + String text = path_edit->get_text().strip_edges(); + if (text.is_empty()) { + text = p_valid_base_name; + } + if (!text.begins_with("res://")) { + if (text.contains("/")) { + return "res://" + text; + } else { + return "res://addons/" + text; + } + } + return text; +} + +void GDExtensionCreateDialog::_bind_methods() { + ADD_SIGNAL(MethodInfo("gdextension_created")); +} + +void GDExtensionCreateDialog::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_VISIBILITY_CHANGED: { + if (is_visible()) { + base_name_edit->grab_focus(); + } + } break; + + case NOTIFICATION_READY: { + connect(SceneStringName(confirmed), callable_mp(this, &GDExtensionCreateDialog::_on_confirmed)); + get_cancel_button()->connect(SceneStringName(pressed), callable_mp(this, &GDExtensionCreateDialog::_on_canceled)); + } break; + } +} + +GDExtensionCreateDialog::GDExtensionCreateDialog() { + get_ok_button()->set_disabled(true); + get_ok_button()->set_text(TTR("Create")); + set_hide_on_ok(true); + set_title(TTR("Create GDExtension")); + + VBoxContainer *vbox = memnew(VBoxContainer); + vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL); + vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_child(vbox); + + GridContainer *grid = memnew(GridContainer); + grid->set_columns(2); + grid->set_v_size_flags(Control::SIZE_EXPAND_FILL); + vbox->add_child(grid); + + // Base name line edit. + Label *base_name_label = memnew(Label); + base_name_label->set_text(TTR("Base Name:")); + base_name_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(base_name_label); + + base_name_edit = memnew(LineEdit); + base_name_edit->set_placeholder("my_extension"); + base_name_edit->set_tooltip_text(TTR("Required. The base name of the extension. Must be valid as an identifier continuation in a programming language.\nThis will be used to determine the library and folder names, if unspecified.\nFor engine modules, this must match the module's folder name.")); + base_name_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(base_name_edit); + + // Library name line edit. + Label *library_name_label = memnew(Label); + library_name_label->set_text(TTR("Library Name:")); + library_name_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(library_name_label); + + library_name_edit = memnew(LineEdit); + library_name_edit->set_placeholder("my_extension"); + library_name_edit->set_tooltip_text(TTR("This should be similar to the base name, but must on its own be a valid identifier in a programming language.\nAs a good practice, this should be a superset of the base name.")); + library_name_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(library_name_edit); + + // Path line edit. + Label *path_label = memnew(Label); + path_label->set_text(TTR("Path:")); + path_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(path_label); + + path_edit = memnew(LineEdit); + path_edit->set_placeholder("res://addons/my_extension"); + path_edit->set_tooltip_text(TTR("The path to the extension (relative to the project root). If you type this manually, res:// will be added for you.")); + path_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(path_edit); + + // Language dropdown. + Label *language_label = memnew(Label); + language_label->set_text(TTR("Language:")); + language_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(language_label); + + language_option = memnew(OptionButton); + language_option->set_tooltip_text(TTR("The language to use for the extension.")); + grid->add_child(language_option); + + // Compile now checkbox. + Label *compile_label = memnew(Label); + compile_label->set_text(TTR("Compile now?")); + compile_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(compile_label); + + compile_checkbox = memnew(CheckBox); + compile_checkbox->set_pressed(true); + grid->add_child(compile_checkbox); + + Control *spacing = memnew(Control); + vbox->add_child(spacing); + spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE)); + + validation_panel = memnew(EditorValidationPanel); + validation_panel->set_custom_minimum_size(Size2(500, 200) * EDSCALE); + validation_panel->add_line(MSG_ID_BASE_NAME, TTR("Please specify a base name.")); + validation_panel->add_line(MSG_ID_LIBRARY_NAME, TTR("Please specify a base name.")); + validation_panel->add_line(MSG_ID_PATH, TTR("Please specify a base name.")); + validation_panel->set_update_callback(callable_mp(this, &GDExtensionCreateDialog::_on_required_text_changed)); + validation_panel->set_accept_button(get_ok_button()); + vbox->add_child(validation_panel); + validation_panel->update(); + + base_name_edit->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + library_name_edit->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + path_edit->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + language_option->connect(SceneStringName(item_selected), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + compile_checkbox->connect(SceneStringName(pressed), callable_mp(validation_panel, &EditorValidationPanel::update)); +} diff --git a/editor/plugins/gdextension/gdextension_create_dialog.h b/editor/plugins/gdextension/gdextension_create_dialog.h new file mode 100644 index 000000000000..24f093c0a796 --- /dev/null +++ b/editor/plugins/gdextension/gdextension_create_dialog.h @@ -0,0 +1,79 @@ +/**************************************************************************/ +/* gdextension_create_dialog.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDEXTENSION_CREATE_DIALOG_H +#define GDEXTENSION_CREATE_DIALOG_H + +#include "scene/gui/dialogs.h" + +class CheckBox; +class EditorValidationPanel; +class GDExtensionCreatorPlugin; +class OptionButton; + +class GDExtensionCreateDialog : public ConfirmationDialog { + GDCLASS(GDExtensionCreateDialog, ConfirmationDialog); + + enum { + MSG_ID_BASE_NAME, + MSG_ID_LIBRARY_NAME, + MSG_ID_PATH, + MSG_ID_MAX, // Individual GDExtension creators may add more messages. + }; + + LineEdit *base_name_edit = nullptr; + LineEdit *library_name_edit = nullptr; + LineEdit *path_edit = nullptr; + OptionButton *language_option = nullptr; + CheckBox *compile_checkbox = nullptr; + + EditorValidationPanel *validation_panel = nullptr; + Vector> plugin_creators; + Vector language_option_index_map; + + void _clear_fields(); + void _on_canceled(); + void _on_confirmed(); + void _on_required_text_changed(); + + String _get_valid_base_name(); + String _get_valid_library_name(const String &p_valid_base_name); + String _get_valid_path(const String &p_valid_base_name); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + void load_plugin_creators(const Vector> &p_plugin_creators); + GDExtensionCreateDialog(); +}; + +#endif // GDEXTENSION_CREATE_DIALOG_H diff --git a/editor/plugins/gdextension/gdextension_creator_plugin.h b/editor/plugins/gdextension/gdextension_creator_plugin.h new file mode 100644 index 000000000000..cfefc929ae8c --- /dev/null +++ b/editor/plugins/gdextension/gdextension_creator_plugin.h @@ -0,0 +1,55 @@ +/**************************************************************************/ +/* gdextension_creator_plugin.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDEXTENSION_CREATOR_PLUGIN_H +#define GDEXTENSION_CREATOR_PLUGIN_H + +#include "core/object/class_db.h" +#include "core/object/ref_counted.h" + +class GDExtensionCreatorPlugin : public RefCounted { + GDCLASS(GDExtensionCreatorPlugin, RefCounted); + +protected: + enum MessageType { + MSG_OK, + MSG_WARNING, + MSG_ERROR, + MSG_INFO, + }; + +public: + virtual void create_gdextension(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) = 0; + virtual void setup_creator() = 0; + virtual PackedStringArray get_language_variations() const = 0; + virtual Dictionary get_validation_messages(const String &p_path, const String &p_base_name, const String &p_library_name, int p_variation_index, bool p_compile) = 0; +}; + +#endif // GDEXTENSION_CREATOR_PLUGIN_H diff --git a/editor/plugins/gdextension/gdextension_edit_dialog.cpp b/editor/plugins/gdextension/gdextension_edit_dialog.cpp new file mode 100644 index 000000000000..49ecdbd5ab29 --- /dev/null +++ b/editor/plugins/gdextension/gdextension_edit_dialog.cpp @@ -0,0 +1,241 @@ +/**************************************************************************/ +/* gdextension_edit_dialog.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdextension_edit_dialog.h" + +#include "core/extension/gdextension_manager.h" +#include "core/version.h" +#include "editor/editor_properties_array_dict.h" +#include "editor/gui/editor_validation_panel.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/check_box.h" +#include "scene/gui/grid_container.h" + +void GDExtensionEditDialog::load_gdextension_config(const String &p_path) { + Ref config; + config.instantiate(); + Error err = config->load(p_path); + ERR_FAIL_COND_MSG(err != OK, "Error loading GDExtension configuration file: " + p_path); + gdextension_path->set_text(p_path); + // Get configuration keys. + entry_symbol_edit->set_text(config->get_value("configuration", "entry_symbol", "")); + compat_max_version_edit->set_text(config->get_value("configuration", "compatibility_maximum", "")); + compat_min_version_edit->set_text(config->get_value("configuration", "compatibility_minimum", VERSION_BRANCH)); + reloadable_checkbox->set_pressed(config->get_value("configuration", "reloadable", false)); + // Update validation. + validation_panel->update(); +} + +void GDExtensionEditDialog::_clear_fields() { + entry_symbol_edit->clear(); + compat_max_version_edit->clear(); + compat_min_version_edit->clear(); +} + +void GDExtensionEditDialog::_on_canceled() { + _clear_fields(); +} + +void GDExtensionEditDialog::_on_confirmed() { + // Start with the old config to avoid erasing custom keys. + const String gdext_res_path = gdextension_path->get_text(); + Ref config; + config.instantiate(); + Error err = config->load(gdext_res_path); + ERR_FAIL_COND_MSG(err != OK, "Error loading GDExtension configuration file: " + gdext_res_path); + // Set configuration keys. + config->set_value("configuration", "entry_symbol", entry_symbol_edit->get_text()); + config->set_value("configuration", "compatibility_minimum", compat_min_version_edit->get_text()); + const String compat_max = compat_max_version_edit->get_text(); + if (!compat_max.is_empty()) { + config->set_value("configuration", "compatibility_maximum", compat_max); + } + const bool is_reloadable = reloadable_checkbox->is_pressed(); + config->set_value("configuration", "reloadable", is_reloadable); + // Save and reload. + config->save(gdext_res_path); + _clear_fields(); + GDExtensionManager *gdext_man = GDExtensionManager::get_singleton(); + gdext_man->get_extension(gdext_res_path)->set_reloadable(is_reloadable); + if (is_reloadable) { + gdext_man->reload_extension(gdext_res_path); + } else { + WARN_PRINT(TTR("Saved GDExtension changes to ") + gdext_res_path + TTR(", but the extension is not marked as reloadable. You may need to restart Godot to apply changes to the extension.")); + } +} + +#define IS_BEYOND_CURRENT_VERSION(m_parts) (m_parts[0] > VERSION_MAJOR || (m_parts[0] == VERSION_MAJOR && (m_parts[1] > VERSION_MINOR || (m_parts[1] == VERSION_MINOR && (m_parts.size() > 2 && m_parts[2] > VERSION_PATCH))))) + +void GDExtensionEditDialog::_on_required_text_changed() { + // Entry symbol must be a valid programming language identifier. + String entry_symbol = entry_symbol_edit->get_text(); + if (entry_symbol.is_valid_identifier()) { + validation_panel->set_message(MSG_ID_ENTRY_SYMBOL_NAME, TTR("Entry symbol is valid."), EditorValidationPanel::MSG_OK); + } else { + validation_panel->set_message(MSG_ID_ENTRY_SYMBOL_NAME, TTR("Entry symbol is not a valid identifier."), EditorValidationPanel::MSG_ERROR); + } + // Compatibility minimum is required to be at least 4.1.0. + String compat_min_text = compat_min_version_edit->get_text(); + Vector compat_min_parts = compat_min_text.split_ints("."); + if (compat_min_parts.size() < 2) { + validation_panel->set_message(MSG_ID_COMPAT_MIN_VERSION, TTR("Compat min version must be in X.Y or X.Y.Z format."), EditorValidationPanel::MSG_ERROR); + } else if (compat_min_parts[0] < 4 || (compat_min_parts[0] == 4 && compat_min_parts[1] == 0)) { + validation_panel->set_message(MSG_ID_COMPAT_MIN_VERSION, TTR("Compat min version must be at least 4.1.0."), EditorValidationPanel::MSG_ERROR); + } else if (IS_BEYOND_CURRENT_VERSION(compat_min_parts)) { + validation_panel->set_message(MSG_ID_COMPAT_MIN_VERSION, TTR("Compat min version cannot be beyond the current Godot version."), EditorValidationPanel::MSG_ERROR); + } else { + validation_panel->set_message(MSG_ID_COMPAT_MIN_VERSION, TTR("Compatibility minimum version is valid."), EditorValidationPanel::MSG_OK); + } + // Compatibility maximum is optional but must be at least 4.3.0 if set. + String compat_max_text = compat_max_version_edit->get_text(); + if (compat_max_text.is_empty()) { + validation_panel->set_message(MSG_ID_COMPAT_MAX_VERSION, TTR("Compatibility maximum version is optional."), EditorValidationPanel::MSG_OK); + } else { + Vector compat_max_parts = compat_max_text.split_ints("."); + if (compat_max_parts.size() < 2) { + validation_panel->set_message(MSG_ID_COMPAT_MAX_VERSION, TTR("Compat max version must be in X.Y or X.Y.Z format."), EditorValidationPanel::MSG_ERROR); + } else if (compat_max_parts[0] < 4 || (compat_max_parts[0] == 4 && compat_max_parts[1] < 3)) { + validation_panel->set_message(MSG_ID_COMPAT_MAX_VERSION, TTR("Compat max version must be at least 4.3.0."), EditorValidationPanel::MSG_ERROR); + } else { + validation_panel->set_message(MSG_ID_COMPAT_MAX_VERSION, TTR("Compatibility maximum version is valid."), EditorValidationPanel::MSG_OK); + } + } +} + +void GDExtensionEditDialog::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: { + connect(SceneStringName(confirmed), callable_mp(this, &GDExtensionEditDialog::_on_confirmed)); + get_cancel_button()->connect(SceneStringName(pressed), callable_mp(this, &GDExtensionEditDialog::_on_canceled)); + } break; + } +} + +GDExtensionEditDialog::GDExtensionEditDialog() { + get_ok_button()->set_disabled(true); + get_ok_button()->set_text(TTR("Save")); + set_hide_on_ok(true); + set_title(TTR("Edit GDExtension")); + set_min_size(Size2(400, 300) * EDSCALE); + + VBoxContainer *vbox = memnew(VBoxContainer); + vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL); + vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_child(vbox); + + ScrollContainer *scroll = memnew(ScrollContainer); + scroll->set_custom_minimum_size(Size2(400, 150) * EDSCALE); + scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED); + scroll->set_h_size_flags(Control::SIZE_EXPAND_FILL); + scroll->set_v_size_flags(Control::SIZE_EXPAND_FILL); + vbox->add_child(scroll); + + GridContainer *grid = memnew(GridContainer); + grid->set_columns(2); + grid->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->set_v_size_flags(Control::SIZE_SHRINK_BEGIN); + scroll->add_child(grid); + + // GDExtension file path (read-only). + Label *gdextension_path_label = memnew(Label); + gdextension_path_label->set_text(TTR("Path:")); + gdextension_path_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(gdextension_path_label); + + gdextension_path = memnew(Label); + gdextension_path->set_text("res://addons/my_extension/my_extension.gdextension"); + gdextension_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(gdextension_path); + + // Entry symbol. + Label *entry_symbol_label = memnew(Label); + entry_symbol_label->set_text(TTR("Entry Symbol:")); + entry_symbol_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(entry_symbol_label); + + entry_symbol_edit = memnew(LineEdit); + entry_symbol_edit->set_placeholder("libname_library_init"); + entry_symbol_edit->set_tooltip_text(TTR("Required. The symbol to use as the entry point for the extension.")); + entry_symbol_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(entry_symbol_edit); + + // Compatibility Min Version + Label *compat_min_version_label = memnew(Label); + compat_min_version_label->set_text(TTR("Compat Min Version:")); + compat_min_version_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(compat_min_version_label); + + compat_min_version_edit = memnew(LineEdit); + compat_min_version_edit->set_placeholder(VERSION_BRANCH); + compat_min_version_edit->set_tooltip_text(TTR("Required. The minimum version of Godot that the extension is compatible with.")); + compat_min_version_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(compat_min_version_edit); + + // Compatibility Max Version + Label *compat_max_version_label = memnew(Label); + compat_max_version_label->set_text(TTR("Compat Max Version:")); + compat_max_version_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(compat_max_version_label); + + compat_max_version_edit = memnew(LineEdit); + compat_max_version_edit->set_placeholder(""); + compat_max_version_edit->set_tooltip_text(TTR("Optional. The maximum version of Godot that the extension is compatible with.")); + compat_max_version_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(compat_max_version_edit); + + // Reloadable checkbox. + Label *reloadable_label = memnew(Label); + reloadable_label->set_text(TTR("Reloadable:")); + reloadable_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + grid->add_child(reloadable_label); + + reloadable_checkbox = memnew(CheckBox); + reloadable_checkbox->set_tooltip_text(TTR("Optional. Whether the extension can be reloaded at runtime. Recommended to be true for most extensions.")); + reloadable_checkbox->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_child(reloadable_checkbox); + + Control *spacing = memnew(Control); + spacing->set_custom_minimum_size(Size2(0, 4 * EDSCALE)); + vbox->add_child(spacing); + + validation_panel = memnew(EditorValidationPanel); + validation_panel->set_custom_minimum_size(Size2(500, 120) * EDSCALE); + validation_panel->set_v_size_flags(Control::SIZE_SHRINK_END); + validation_panel->add_line(MSG_ID_ENTRY_SYMBOL_NAME, TTR("Entry symbol is valid.")); + validation_panel->add_line(MSG_ID_COMPAT_MIN_VERSION, TTR("Compatibility minimum version is valid.")); + validation_panel->add_line(MSG_ID_COMPAT_MAX_VERSION, TTR("Compatibility maximum version is valid.")); + validation_panel->set_update_callback(callable_mp(this, &GDExtensionEditDialog::_on_required_text_changed)); + validation_panel->set_accept_button(get_ok_button()); + vbox->add_child(validation_panel); + + entry_symbol_edit->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + compat_min_version_edit->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + compat_max_version_edit->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); +} diff --git a/editor/plugins/gdextension/gdextension_edit_dialog.h b/editor/plugins/gdextension/gdextension_edit_dialog.h new file mode 100644 index 000000000000..b6c8b3e0475b --- /dev/null +++ b/editor/plugins/gdextension/gdextension_edit_dialog.h @@ -0,0 +1,70 @@ +/**************************************************************************/ +/* gdextension_edit_dialog.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDEXTENSION_EDIT_DIALOG_H +#define GDEXTENSION_EDIT_DIALOG_H + +#include "scene/gui/dialogs.h" + +class CheckBox; +class EditorPropertyDictionary; +class EditorValidationPanel; +class LineEdit; + +class GDExtensionEditDialog : public ConfirmationDialog { + GDCLASS(GDExtensionEditDialog, ConfirmationDialog); + + enum { + MSG_ID_ENTRY_SYMBOL_NAME, + MSG_ID_COMPAT_MIN_VERSION, + MSG_ID_COMPAT_MAX_VERSION, + }; + + Label *gdextension_path = nullptr; + LineEdit *entry_symbol_edit = nullptr; + LineEdit *compat_max_version_edit = nullptr; + LineEdit *compat_min_version_edit = nullptr; + CheckBox *reloadable_checkbox = nullptr; + EditorValidationPanel *validation_panel = nullptr; + + void _clear_fields(); + void _on_canceled(); + void _on_confirmed(); + void _on_required_text_changed(); + +protected: + void _notification(int p_what); + +public: + void load_gdextension_config(const String &p_path); + GDExtensionEditDialog(); +}; + +#endif // GDEXTENSION_EDIT_DIALOG_H diff --git a/editor/plugins/gdextension_export_plugin.h b/editor/plugins/gdextension/gdextension_export_plugin.h similarity index 100% rename from editor/plugins/gdextension_export_plugin.h rename to editor/plugins/gdextension/gdextension_export_plugin.h diff --git a/editor/plugins/gdextension/project_settings_gdextension.cpp b/editor/plugins/gdextension/project_settings_gdextension.cpp new file mode 100644 index 000000000000..f145e74a7254 --- /dev/null +++ b/editor/plugins/gdextension/project_settings_gdextension.cpp @@ -0,0 +1,137 @@ +/**************************************************************************/ +/* project_settings_gdextension.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "project_settings_gdextension.h" + +#include "cpp_scons/cpp_scons_gdext_creator.h" +#include "gdextension_create_dialog.h" +#include "gdextension_creator_plugin.h" +#include "gdextension_edit_dialog.h" + +#include "core/extension/gdextension_manager.h" +#include "editor/editor_file_system.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/tree.h" + +class GDExtensionPluginCreatorBase; + +const int BUTTON_EDIT = 0; + +void ProjectSettingsGDExtension::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_WM_WINDOW_FOCUS_IN: { + _update_extension_tree(); + } break; + case Node::NOTIFICATION_READY: { + extension_list->connect("button_clicked", callable_mp(this, &ProjectSettingsGDExtension::_cell_button_pressed)); + } break; + } +} + +void ProjectSettingsGDExtension::_cell_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) { + if (p_button != MouseButton::LEFT) { + return; + } + TreeItem *item = Object::cast_to(p_item); + if (!item) { + return; + } + if (p_id == BUTTON_EDIT && p_column == COLUMN_EDIT) { + String path = item->get_metadata(COLUMN_PATH); + config_dialog->load_gdextension_config(path); + config_dialog->popup_centered(Size2(400, 300) * EDSCALE); + } +} + +void ProjectSettingsGDExtension::_update_extension_tree() { + extension_list->clear(); + TreeItem *root = extension_list->create_item(); + Vector extensions = GDExtensionManager::get_singleton()->get_loaded_extensions(); + for (const String &extension : extensions) { + TreeItem *item = extension_list->create_item(root); + item->set_text(COLUMN_PATH, extension); + item->add_button(COLUMN_EDIT, get_editor_theme_icon(SNAME("Edit")), BUTTON_EDIT, false, TTR("Edit Extension")); + item->set_metadata(COLUMN_PATH, extension); + } +} + +ProjectSettingsGDExtension::ProjectSettingsGDExtension() { + create_dialog = memnew(GDExtensionCreateDialog); + add_child(create_dialog); + create_dialog->connect("gdextension_created", callable_mp(this, &ProjectSettingsGDExtension::_on_gdextension_created)); + config_dialog = memnew(GDExtensionEditDialog); + add_child(config_dialog); + + HBoxContainer *title_hb = memnew(HBoxContainer); + Label *label = memnew(Label(TTR("Installed GDExtensions:"))); + label->set_theme_type_variation("HeaderSmall"); + title_hb->add_child(label); + title_hb->add_spacer(); + Button *create_plugin_button = memnew(Button(TTR("Create GDExtension"))); + create_plugin_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectSettingsGDExtension::_on_create_gdextension_pressed)); + title_hb->add_child(create_plugin_button); + add_child(title_hb); + + extension_list = memnew(Tree); + extension_list->set_v_size_flags(SIZE_EXPAND_FILL); + extension_list->set_hide_root(true); + extension_list->set_columns(COLUMN_MAX); + extension_list->set_column_titles_visible(true); + extension_list->set_column_title(COLUMN_PATH, TTR("Path")); + extension_list->set_column_title(COLUMN_EDIT, TTR("Edit")); + extension_list->set_column_title_alignment(COLUMN_PATH, HORIZONTAL_ALIGNMENT_LEFT); + extension_list->set_column_title_alignment(COLUMN_EDIT, HORIZONTAL_ALIGNMENT_LEFT); + extension_list->set_column_expand(COLUMN_PADDING_LEFT, false); + extension_list->set_column_expand(COLUMN_PATH, true); + extension_list->set_column_expand(COLUMN_EDIT, false); + extension_list->set_column_expand(COLUMN_PADDING_RIGHT, false); + extension_list->set_column_clip_content(COLUMN_PADDING_LEFT, true); + extension_list->set_column_clip_content(COLUMN_PATH, true); + extension_list->set_column_clip_content(COLUMN_EDIT, true); + extension_list->set_column_clip_content(COLUMN_PADDING_RIGHT, true); + extension_list->set_column_custom_minimum_width(COLUMN_PADDING_LEFT, 10 * EDSCALE); + extension_list->set_column_custom_minimum_width(COLUMN_PATH, 300 * EDSCALE); + extension_list->set_column_custom_minimum_width(COLUMN_EDIT, 40 * EDSCALE); + extension_list->set_column_custom_minimum_width(COLUMN_PADDING_RIGHT, 10 * EDSCALE); + + add_child(extension_list); +} + +void ProjectSettingsGDExtension::_on_create_gdextension_pressed() { + Vector> creators; + creators.append(memnew(CppSconsGDExtensionCreator)); + create_dialog->load_plugin_creators(creators); + create_dialog->popup_centered(Size2(400, 300) * EDSCALE); +} + +void ProjectSettingsGDExtension::_on_gdextension_created() { + EditorFileSystem::get_singleton()->scan(); + _update_extension_tree(); +} diff --git a/editor/plugins/gdextension/project_settings_gdextension.h b/editor/plugins/gdextension/project_settings_gdextension.h new file mode 100644 index 000000000000..41cb1ef1f0f1 --- /dev/null +++ b/editor/plugins/gdextension/project_settings_gdextension.h @@ -0,0 +1,67 @@ +/**************************************************************************/ +/* project_settings_gdextension.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef PROJECT_SETTINGS_GDEXTENSION_H +#define PROJECT_SETTINGS_GDEXTENSION_H + +#include "scene/gui/box_container.h" + +class GDExtensionCreateDialog; +class GDExtensionEditDialog; +class Tree; + +class ProjectSettingsGDExtension : public VBoxContainer { + GDCLASS(ProjectSettingsGDExtension, VBoxContainer); + + enum { + COLUMN_PADDING_LEFT, + COLUMN_PATH, + COLUMN_EDIT, + COLUMN_PADDING_RIGHT, + COLUMN_MAX, + }; + + GDExtensionCreateDialog *create_dialog = nullptr; + GDExtensionEditDialog *config_dialog = nullptr; + Tree *extension_list = nullptr; + + void _on_create_gdextension_pressed(); + void _on_gdextension_created(); + void _cell_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button); + void _update_extension_tree(); + +protected: + void _notification(int p_what); + +public: + ProjectSettingsGDExtension(); +}; + +#endif // PROJECT_SETTINGS_GDEXTENSION_H diff --git a/editor/plugins/plugin_config_dialog.h b/editor/plugins/plugin_config_dialog.h index 7d6eab5e1875..48553373ba2d 100644 --- a/editor/plugins/plugin_config_dialog.h +++ b/editor/plugins/plugin_config_dialog.h @@ -77,7 +77,7 @@ class PluginConfigDialog : public ConfirmationDialog { static String _to_absolute_plugin_path(const String &p_plugin_name); protected: - virtual void _notification(int p_what); + void _notification(int p_what); static void _bind_methods(); public: diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 489fbb037ff1..2c68995c5c68 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -37,6 +37,7 @@ #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/export/editor_export.h" +#include "editor/plugins/gdextension/project_settings_gdextension.h" #include "editor/themes/editor_scale.h" #include "scene/gui/check_button.h" #include "servers/movie_writer/movie_writer.h" @@ -54,7 +55,7 @@ void ProjectSettingsEditor::popup_project_settings(bool p_clear_filter) { if (saved_size != Rect2()) { popup(saved_size); } else { - popup_centered_clamped(Size2(1200, 700) * EDSCALE, 0.8); + popup_centered_clamped(Size2(1300, 700) * EDSCALE, 0.8); } _add_feature_overrides(); @@ -759,6 +760,10 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { plugin_settings->set_name(TTR("Plugins")); tab_container->add_child(plugin_settings); + gdextension_settings = memnew(ProjectSettingsGDExtension); + gdextension_settings->set_name(TTR("GDExtension")); + tab_container->add_child(gdextension_settings); + timer = memnew(Timer); timer->set_wait_time(1.5); timer->connect("timeout", callable_mp(ps, &ProjectSettings::save)); diff --git a/editor/project_settings_editor.h b/editor/project_settings_editor.h index 5890ed2c2dd7..451c6de452f8 100644 --- a/editor/project_settings_editor.h +++ b/editor/project_settings_editor.h @@ -44,6 +44,7 @@ #include "scene/gui/tab_container.h" class FileSystemDock; +class ProjectSettingsGDExtension; class ProjectSettingsEditor : public AcceptDialog { GDCLASS(ProjectSettingsEditor, AcceptDialog); @@ -61,6 +62,7 @@ class ProjectSettingsEditor : public AcceptDialog { ShaderGlobalsEditor *shaders_global_shader_uniforms_editor = nullptr; GroupSettingsEditor *group_settings = nullptr; EditorPluginSettings *plugin_settings = nullptr; + ProjectSettingsGDExtension *gdextension_settings = nullptr; LineEdit *search_box = nullptr; CheckButton *advanced = nullptr; diff --git a/editor/window_wrapper.cpp b/editor/window_wrapper.cpp index 9496ba016cd0..832406d9008c 100644 --- a/editor/window_wrapper.cpp +++ b/editor/window_wrapper.cpp @@ -47,7 +47,7 @@ class ShortcutBin : public Node { GDCLASS(ShortcutBin, Node); - virtual void _notification(int what) { + void _notification(int what) { switch (what) { case NOTIFICATION_READY: set_process_shortcut_input(true); diff --git a/misc/scripts/header_guards.py b/misc/scripts/header_guards.py index fed418db1ed8..c94a9be87957 100755 --- a/misc/scripts/header_guards.py +++ b/misc/scripts/header_guards.py @@ -12,6 +12,8 @@ invalid = [] for file in sys.argv[1:]: + if file.startswith("editor/plugins/gdextension/cpp_scons/template/"): + continue header_start = -1 HEADER_CHECK_OFFSET = -1 diff --git a/scene/3d/skeleton_ik_3d.h b/scene/3d/skeleton_ik_3d.h index 5d6020194e14..751ac373f74d 100644 --- a/scene/3d/skeleton_ik_3d.h +++ b/scene/3d/skeleton_ik_3d.h @@ -143,7 +143,7 @@ class SkeletonIK3D : public SkeletonModifier3D { void _validate_property(PropertyInfo &p_property) const; static void _bind_methods(); - virtual void _notification(int p_what); + void _notification(int p_what); virtual void _process_modification() override; diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index b47e5b1eb915..dd4972f258fb 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -1863,7 +1863,7 @@ TEST_CASE("[String] validate_node_name") { CHECK(name_with_invalid_chars.validate_node_name() == "Name with invalid characters ____removed!"); } -TEST_CASE("[String] validate_identifier") { +TEST_CASE("[String] validate_ascii_identifier") { String empty_string; CHECK(empty_string.validate_ascii_identifier() == "_");