diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index 5b9377fe5947..3d1379b90869 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -400,6 +400,7 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = { { "ui_filedialog_show_hidden", TTRC("Show Hidden") }, { "ui_swap_input_direction ", TTRC("Swap Input Direction") }, { "ui_unicode_start", TTRC("Start Unicode Character Input") }, + { "ui_toggle_licenses_dialog", TTRC("Toggle License Notices") }, { "", ""} /* clang-format on */ }; @@ -785,6 +786,12 @@ const HashMap>> &InputMap::get_builtins() { inputs.push_back(InputEventKey::create_reference(Key::QUOTELEFT | KeyModifierMask::CMD_OR_CTRL)); default_builtin_cache.insert("ui_swap_input_direction", inputs); + // ///// UI Misc Shortcuts ///// + + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::L | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT)); + default_builtin_cache.insert("ui_toggle_licenses_dialog", inputs); + return default_builtin_cache; } diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 4d41d2e94afb..a2b7a6a975a7 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1388,6 +1388,10 @@ Default [InputEventAction] to toggle [i]insert mode[/i] in a text field. While in insert mode, inserting new text overrides the character after the cursor, unless the next character is a new line. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + Toggles the built-in dialog which displays Godot's third-party notices. This can also be toggled from a script using [member SceneTree.licenses_dialog_visible]. + Since the default shortcut is not usable on mobile platforms, it is recommended to create a button that sets [member SceneTree.licenses_dialog_visible] to [code]true[/code] when pressed in your project's menus. See [url=$DOCS_URL/complying_with_licenses.html]Complying with licenses[/url] in the documentation for more information. + Default [InputEventAction] to undo the most recent action. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. diff --git a/doc/classes/SceneTree.xml b/doc/classes/SceneTree.xml index 77baef9d0898..1b3c9201b230 100644 --- a/doc/classes/SceneTree.xml +++ b/doc/classes/SceneTree.xml @@ -255,6 +255,9 @@ The root of the scene currently being edited in the editor. This is usually a direct child of [member root]. [b]Note:[/b] This property does nothing in release builds. + + If [code]true[/code], shows the built-in dialog which displays Godot's third-party notices. This dialog can also be toggled by pressing the [member ProjectSettings.input/ui_toggle_licenses_dialog] built-in action. See [url=$DOCS_URL/complying_with_licenses.html]Complying with licenses[/url] in the documentation for more information. + If [code]true[/code] (default value), enables automatic polling of the [MultiplayerAPI] for this SceneTree during [signal process_frame]. If [code]false[/code], you need to manually call [method MultiplayerAPI.poll] to process network packets and deliver RPCs. This allows running RPCs in a different loop (e.g. physics, thread, specific time step) and for manual [Mutex] protection when accessing the [MultiplayerAPI] from threads. diff --git a/editor/icons/LicensesDialog.svg b/editor/icons/LicensesDialog.svg new file mode 100644 index 000000000000..77c55524d12d --- /dev/null +++ b/editor/icons/LicensesDialog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scene/gui/licenses_dialog.cpp b/scene/gui/licenses_dialog.cpp new file mode 100644 index 000000000000..f89cf052e344 --- /dev/null +++ b/scene/gui/licenses_dialog.cpp @@ -0,0 +1,146 @@ +/**************************************************************************/ +/* licenses_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 "licenses_dialog.h" + +#include "box_container.h" +#include "button.h" +#include "core/license.gen.h" +#include "core/string/string_buffer.h" +#include "label.h" +#include "margin_container.h" +#include "panel_container.h" +#include "rich_text_label.h" +#include "scene/main/canvas_layer.h" +#include "scene/resources/style_box_flat.h" + +void LicensesDialog::_close_button_pressed() { + SceneTree::get_singleton()->set_licenses_dialog_visible(false); +} + +void LicensesDialog::unhandled_key_input(const Ref &p_event) { + if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { + SceneTree::get_singleton()->set_licenses_dialog_visible(false); + Node::get_viewport()->set_input_as_handled(); + } +} + +LicensesDialog::LicensesDialog() { + // Set on the highest layer, so that nothing else can draw on top. + set_layer(128); + + set_process_unhandled_key_input(true); + + MarginContainer *margin_container = memnew(MarginContainer); + margin_container->set_anchors_preset(Control::PRESET_FULL_RECT); + const float default_base_scale = margin_container->get_theme_default_base_scale(); + const float default_font_size = margin_container->get_theme_default_font_size(); + margin_container->add_theme_constant_override("margin_top", Math::round(20 * default_base_scale)); + margin_container->add_theme_constant_override("margin_right", Math::round(20 * default_base_scale)); + margin_container->add_theme_constant_override("margin_bottom", Math::round(20 * default_base_scale)); + margin_container->add_theme_constant_override("margin_left", Math::round(20 * default_base_scale)); + add_child(margin_container); + + PanelContainer *panel_container = memnew(PanelContainer); + margin_container->add_child(panel_container); + + MarginContainer *inner_margin_container = memnew(MarginContainer); + inner_margin_container->add_theme_constant_override("margin_top", Math::round(10 * default_base_scale)); + inner_margin_container->add_theme_constant_override("margin_right", Math::round(10 * default_base_scale)); + inner_margin_container->add_theme_constant_override("margin_bottom", Math::round(10 * default_base_scale)); + inner_margin_container->add_theme_constant_override("margin_left", Math::round(10 * default_base_scale)); + panel_container->add_child(inner_margin_container); + + VBoxContainer *vbox_container = memnew(VBoxContainer); + vbox_container->add_theme_constant_override("separation", Math::round(10 * default_base_scale)); + inner_margin_container->add_child(vbox_container); + + Label *title_label = memnew(Label); + title_label->set_text(RTR("Third-party notices")); + title_label->add_theme_font_size_override(SceneStringName(font_size), Math::round(1.333 * default_font_size)); + title_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + vbox_container->add_child(title_label); + + // Based on `editor_about.cpp` with references to TreeItem removed, + // as we only have the "All Components" view here. A preamble is also added. + StringBuffer<> long_text; + long_text += RTR("This project is powered by Godot Engine, which relies on a number of third-party free and open source libraries, all compatible with the terms of its MIT license. The following is an exhaustive list of all such third-party components with their respective copyright statements and license terms.") + "\n\n"; + + long_text += RTR("Components:") + "\n\n"; + + for (int component_index = 0; component_index < COPYRIGHT_INFO_COUNT; component_index++) { + const ComponentCopyright &component = COPYRIGHT_INFO[component_index]; + const String component_name = String::utf8(component.name); + long_text += "- " + component_name; + for (int part_index = 0; part_index < component.part_count; part_index++) { + const ComponentCopyrightPart &part = component.parts[part_index]; + String copyright; + for (int copyright_index = 0; copyright_index < part.copyright_count; copyright_index++) { + copyright += String::utf8("\n \xc2\xa9 ") + String::utf8(part.copyright_statements[copyright_index]); + } + long_text += copyright; + String license = "\n License: " + String::utf8(part.license) + "\n"; + long_text += license + "\n\n"; + } + } + + long_text += RTR("Licenses:") + "\n\n"; + + for (int i = 0; i < LICENSE_COUNT; i++) { + const String licensename = String::utf8(LICENSE_NAMES[i]); + long_text += "- " + licensename + "\n"; + const String licensebody = String::utf8(LICENSE_BODIES[i]); + long_text += " " + licensebody.replace("\n", "\n ") + "\n\n"; + } + + RichTextLabel *rich_text_label = memnew(RichTextLabel); + rich_text_label->set_text(long_text); + rich_text_label->set_threaded(true); + rich_text_label->set_v_size_flags(Control::SIZE_EXPAND_FILL); + rich_text_label->set_focus_mode(Control::FOCUS_ALL); + + // Add a background to the scrollable area with the license text. + Ref background; + background.instantiate(); + background->set_bg_color(Color(0, 0, 0, 0.25)); + background->set_content_margin_all(Math::round(10 * default_base_scale)); + rich_text_label->add_theme_style_override(CoreStringName(normal), background); + + vbox_container->add_child(rich_text_label); + // Allow for keyboard navigation by grabbing focus immediately on the scrollable control. + callable_mp((Control *)rich_text_label, &Control::grab_focus).call_deferred(); + + Button *close_button = memnew(Button); + close_button->set_text(RTR("Close")); + close_button->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + close_button->set_custom_minimum_size(Vector2(100, 40) * default_base_scale); + close_button->connect(SceneStringName(pressed), callable_mp(this, &LicensesDialog::_close_button_pressed)); + vbox_container->add_child(close_button); +} diff --git a/scene/gui/licenses_dialog.h b/scene/gui/licenses_dialog.h new file mode 100644 index 000000000000..6e7dfb952713 --- /dev/null +++ b/scene/gui/licenses_dialog.h @@ -0,0 +1,48 @@ +/**************************************************************************/ +/* licenses_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 LICENSES_DIALOG_H +#define LICENSES_DIALOG_H + +#include "scene/main/canvas_layer.h" + +class LicensesDialog : public CanvasLayer { + GDCLASS(LicensesDialog, CanvasLayer); + + void _close_button_pressed(); + +protected: + virtual void unhandled_key_input(const Ref &p_event) override; + +public: + LicensesDialog(); +}; + +#endif // LICENSES_DIALOG_H diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index 71d91b970eea..e4ea4ac32985 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -46,6 +46,7 @@ #include "scene/animation/tween.h" #include "scene/debugger/scene_debugger.h" #include "scene/gui/control.h" +#include "scene/gui/licenses_dialog.h" #include "scene/main/multiplayer_api.h" #include "scene/main/viewport.h" #include "scene/resources/environment.h" @@ -1649,6 +1650,36 @@ bool SceneTree::is_multiplayer_poll_enabled() const { return multiplayer_poll; } +void SceneTree::set_licenses_dialog_visible(bool p_visible) { + if (p_visible) { + if (licenses_dialog == nullptr) { + licenses_dialog = memnew(LicensesDialog); + // Begin name with an underscore to avoid conflict with project nodes. + licenses_dialog->set_name("_LicensesDialog"); + get_root()->add_child(licenses_dialog, false, Node::INTERNAL_MODE_BACK); + } else { + ERR_PRINT("Licenses dialog already exists."); + } + } else { + if (licenses_dialog != nullptr) { + // Free when closing to avoid reserving memory during the project's run duration. + licenses_dialog->queue_free(); + licenses_dialog = nullptr; + } else { + ERR_PRINT("Couldn't find licenses dialog to hide."); + } + } +} + +bool SceneTree::is_licenses_dialog_visible() const { + if (licenses_dialog) { + return licenses_dialog->is_visible(); + } + + // Licenses dialog isn't created yet. Therefore, it's not visible. + return false; +} + void SceneTree::_bind_methods() { ClassDB::bind_method(D_METHOD("get_root"), &SceneTree::get_root); ClassDB::bind_method(D_METHOD("has_group", "name"), &SceneTree::has_group); @@ -1723,6 +1754,9 @@ void SceneTree::_bind_methods() { ClassDB::bind_method(D_METHOD("set_multiplayer_poll_enabled", "enabled"), &SceneTree::set_multiplayer_poll_enabled); ClassDB::bind_method(D_METHOD("is_multiplayer_poll_enabled"), &SceneTree::is_multiplayer_poll_enabled); + ClassDB::bind_method(D_METHOD("set_licenses_dialog_visible", "visible"), &SceneTree::set_licenses_dialog_visible); + ClassDB::bind_method(D_METHOD("is_licenses_dialog_visible"), &SceneTree::is_licenses_dialog_visible); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_accept_quit"), "set_auto_accept_quit", "is_auto_accept_quit"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "quit_on_go_back"), "set_quit_on_go_back", "is_quit_on_go_back"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "debug_collisions_hint"), "set_debug_collisions_hint", "is_debugging_collisions_hint"); @@ -1734,6 +1768,7 @@ void SceneTree::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "root", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "", "get_root"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "multiplayer_poll"), "set_multiplayer_poll_enabled", "is_multiplayer_poll_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "physics_interpolation"), "set_physics_interpolation_enabled", "is_physics_interpolation_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "licenses_dialog_visible"), "set_licenses_dialog_visible", "is_licenses_dialog_visible"); ADD_SIGNAL(MethodInfo("tree_changed")); ADD_SIGNAL(MethodInfo("tree_process_mode_changed")); //editor only signal, but due to API hash it can't be removed in run-time diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index 7e445411050f..44fe1dd2a7b1 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -44,6 +44,7 @@ class Node; #ifndef _3D_DISABLED class Node3D; #endif +class LicensesDialog; class Window; class Material; class Mesh; @@ -190,6 +191,9 @@ class SceneTree : public MainLoop { Node *prev_scene = nullptr; Node *pending_new_scene = nullptr; + // Initialized lazily and destroyed eagerly to decrease RAM usage, since it contains a lot of text. + LicensesDialog *licenses_dialog = nullptr; + Color debug_collisions_color; Color debug_collision_contact_color; Color debug_paths_color; @@ -426,6 +430,9 @@ class SceneTree : public MainLoop { void set_multiplayer_poll_enabled(bool p_enabled); bool is_multiplayer_poll_enabled() const; + void set_licenses_dialog_visible(bool p_visible); + bool is_licenses_dialog_visible() const; + static void add_idle_callback(IdleCallback p_callback); void set_disable_node_threading(bool p_disable); diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 54f66e8d4e68..ea9c4b8338ad 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -2106,6 +2106,10 @@ void Viewport::_gui_input_event(Ref p_event) { } } + if (!Engine::get_singleton()->is_editor_hint() && !Engine::get_singleton()->is_project_manager_hint() && p_event->is_action_pressed("ui_toggle_licenses_dialog")) { + SceneTree::get_singleton()->set_licenses_dialog_visible(!SceneTree::get_singleton()->is_licenses_dialog_visible()); + } + if (gui.key_focus && !gui.key_focus->is_visible_in_tree()) { gui.key_focus->release_focus(); }