Introduce the concept of global theme contexts

This commit adds the default theme context, which replaces
the need to manually check the project and the default theme
all the time; simplifies related code.

It also adds framework for custom theme contexts, to be used
by the editor. Custom contexts can be attached to any node,
and not necessarily a GUI/Window node. Contexts do no break
theme inheritance and only define which global themes a node
uses as a fallback.

Contexts propagate NOTIFICATION_THEME_CHANGED when one of their
global themes changes. This ensures that global themes act just
like themes assigned to individual nodes and can be previewed
live in the editor.
This commit is contained in:
Yuri Sizov 2023-09-06 16:11:05 +02:00
parent 8449592d92
commit 58126e479c
13 changed files with 487 additions and 246 deletions

View File

@ -780,14 +780,22 @@ Ref<Font> Label3D::_get_font_or_default() const {
return font_override;
}
// Check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
StringName theme_name = "font";
List<StringName> theme_types;
ThemeDB::get_singleton()->get_project_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
ThemeDB::get_singleton()->get_native_type_dependencies(get_class_name(), &theme_types);
ThemeContext *global_context = ThemeDB::get_singleton()->get_default_theme_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_null()) {
continue;
}
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
Ref<Font> f = ThemeDB::get_singleton()->get_project_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (!theme->has_font(theme_name, E)) {
continue;
}
Ref<Font> f = theme->get_font(theme_name, E);
if (f.is_valid()) {
theme_font = f;
theme_font->connect_changed(callable_mp(const_cast<Label3D *>(this), &Label3D::_font_changed));
@ -795,27 +803,8 @@ Ref<Font> Label3D::_get_font_or_default() const {
return f;
}
}
}
// Lastly, fall back on the items defined in the default Theme, if they exist.
{
List<StringName> theme_types;
ThemeDB::get_singleton()->get_default_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_default_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
Ref<Font> f = ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (f.is_valid()) {
theme_font = f;
theme_font->connect_changed(callable_mp(const_cast<Label3D *>(this), &Label3D::_font_changed));
}
return f;
}
}
}
// If they don't exist, use any type to return the default/empty value.
Ref<Font> f = ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", StringName());
Ref<Font> f = global_context->get_fallback_theme()->get_font(theme_name, StringName());
if (f.is_valid()) {
theme_font = f;
theme_font->connect_changed(callable_mp(const_cast<Label3D *>(this), &Label3D::_font_changed));

View File

@ -2465,6 +2465,11 @@ bool Control::has_theme_owner_node() const {
return data.theme_owner->has_owner_node();
}
void Control::set_theme_context(ThemeContext *p_context, bool p_propagate) {
ERR_MAIN_THREAD_GUARD;
data.theme_owner->set_owner_context(p_context, p_propagate);
}
void Control::set_theme(const Ref<Theme> &p_theme) {
ERR_MAIN_THREAD_GUARD;
if (data.theme == p_theme) {
@ -3124,7 +3129,9 @@ void Control::_notification(int p_notification) {
notification(NOTIFICATION_TRANSLATION_CHANGED);
}
#endif
notification(NOTIFICATION_THEME_CHANGED);
// Emits NOTIFICATION_THEME_CHANGED internally.
set_theme_context(ThemeDB::get_singleton()->get_nearest_theme_context(this));
} break;
case NOTIFICATION_POST_ENTER_TREE: {
@ -3134,6 +3141,7 @@ void Control::_notification(int p_notification) {
} break;
case NOTIFICATION_EXIT_TREE: {
set_theme_context(nullptr, false);
release_focus();
get_viewport()->_gui_remove_control(this);
} break;
@ -3632,7 +3640,7 @@ void Control::_bind_methods() {
}
Control::Control() {
data.theme_owner = memnew(ThemeOwner);
data.theme_owner = memnew(ThemeOwner(this));
}
Control::~Control() {

View File

@ -42,6 +42,7 @@ class Viewport;
class Label;
class Panel;
class ThemeOwner;
class ThemeContext;
class Control : public CanvasItem {
GDCLASS(Control, CanvasItem);
@ -553,6 +554,8 @@ public:
Node *get_theme_owner_node() const;
bool has_theme_owner_node() const;
void set_theme_context(ThemeContext *p_context, bool p_propagate = true);
void set_theme(const Ref<Theme> &p_theme);
Ref<Theme> get_theme() const;

View File

@ -1269,7 +1269,9 @@ void Window::_notification(int p_what) {
notification(NOTIFICATION_TRANSLATION_CHANGED);
}
#endif
notification(NOTIFICATION_THEME_CHANGED);
// Emits NOTIFICATION_THEME_CHANGED internally.
set_theme_context(ThemeDB::get_singleton()->get_nearest_theme_context(this));
} break;
case NOTIFICATION_READY: {
@ -1313,6 +1315,8 @@ void Window::_notification(int p_what) {
} break;
case NOTIFICATION_EXIT_TREE: {
set_theme_context(nullptr, false);
if (transient) {
_clear_transient();
}
@ -1889,6 +1893,11 @@ bool Window::has_theme_owner_node() const {
return theme_owner->has_owner_node();
}
void Window::set_theme_context(ThemeContext *p_context, bool p_propagate) {
ERR_MAIN_THREAD_GUARD;
theme_owner->set_owner_context(p_context, p_propagate);
}
void Window::set_theme(const Ref<Theme> &p_theme) {
ERR_MAIN_THREAD_GUARD;
if (theme == p_theme) {
@ -2887,7 +2896,7 @@ Window::Window() {
max_size_used = max_size; // Update max_size_used.
}
theme_owner = memnew(ThemeOwner);
theme_owner = memnew(ThemeOwner(this));
RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_DISABLED);
}

View File

@ -39,6 +39,7 @@ class Font;
class Shortcut;
class StyleBox;
class ThemeOwner;
class ThemeContext;
class Window : public Viewport {
GDCLASS(Window, Viewport)
@ -365,6 +366,8 @@ public:
Node *get_theme_owner_node() const;
bool has_theme_owner_node() const;
void set_theme_context(ThemeContext *p_context, bool p_propagate = true);
void set_theme(const Ref<Theme> &p_theme);
Ref<Theme> get_theme() const;

View File

@ -2779,34 +2779,22 @@ Ref<Font> FontVariation::_get_base_font_or_default() const {
return base_font;
}
// Check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
StringName theme_name = "font";
List<StringName> theme_types;
ThemeDB::get_singleton()->get_project_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
ThemeDB::get_singleton()->get_native_type_dependencies(get_class_name(), &theme_types);
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
Ref<Font> f = ThemeDB::get_singleton()->get_project_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (f == this) {
ThemeContext *global_context = ThemeDB::get_singleton()->get_default_theme_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_null()) {
continue;
}
if (f.is_valid()) {
theme_font = f;
theme_font->connect_changed(callable_mp(reinterpret_cast<Font *>(const_cast<FontVariation *>(this)), &Font::_invalidate_rids), CONNECT_REFERENCE_COUNTED);
}
return f;
}
}
}
// Lastly, fall back on the items defined in the default Theme, if they exist.
if (ThemeDB::get_singleton()->get_default_theme().is_valid()) {
List<StringName> theme_types;
ThemeDB::get_singleton()->get_default_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_default_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
Ref<Font> f = ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (!theme->has_font(theme_name, E)) {
continue;
}
Ref<Font> f = theme->get_font(theme_name, E);
if (f == this) {
continue;
}
@ -2818,8 +2806,7 @@ Ref<Font> FontVariation::_get_base_font_or_default() const {
}
}
// If they don't exist, use any type to return the default/empty value.
Ref<Font> f = ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", StringName());
Ref<Font> f = global_context->get_fallback_theme()->get_font(theme_name, StringName());
if (f != this) {
if (f.is_valid()) {
theme_font = f;
@ -2827,7 +2814,6 @@ Ref<Font> FontVariation::_get_base_font_or_default() const {
}
return f;
}
}
return Ref<Font>();
}
@ -3131,34 +3117,22 @@ Ref<Font> SystemFont::_get_base_font_or_default() const {
return base_font;
}
// Check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
StringName theme_name = "font";
List<StringName> theme_types;
ThemeDB::get_singleton()->get_project_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
ThemeDB::get_singleton()->get_native_type_dependencies(get_class_name(), &theme_types);
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
Ref<Font> f = ThemeDB::get_singleton()->get_project_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (f == this) {
ThemeContext *global_context = ThemeDB::get_singleton()->get_default_theme_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_null()) {
continue;
}
if (f.is_valid()) {
theme_font = f;
theme_font->connect_changed(callable_mp(reinterpret_cast<Font *>(const_cast<SystemFont *>(this)), &Font::_invalidate_rids), CONNECT_REFERENCE_COUNTED);
}
return f;
}
}
}
// Lastly, fall back on the items defined in the default Theme, if they exist.
if (ThemeDB::get_singleton()->get_default_theme().is_valid()) {
List<StringName> theme_types;
ThemeDB::get_singleton()->get_default_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_default_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
Ref<Font> f = ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (!theme->has_font(theme_name, E)) {
continue;
}
Ref<Font> f = theme->get_font(theme_name, E);
if (f == this) {
continue;
}
@ -3170,8 +3144,7 @@ Ref<Font> SystemFont::_get_base_font_or_default() const {
}
}
// If they don't exist, use any type to return the default/empty value.
Ref<Font> f = ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", StringName());
Ref<Font> f = global_context->get_fallback_theme()->get_font(theme_name, StringName());
if (f != this) {
if (f.is_valid()) {
theme_font = f;
@ -3179,7 +3152,6 @@ Ref<Font> SystemFont::_get_base_font_or_default() const {
}
return f;
}
}
return Ref<Font>();
}

View File

@ -3466,32 +3466,24 @@ Ref<Font> TextMesh::_get_font_or_default() const {
return font_override;
}
// Check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
StringName theme_name = "font";
List<StringName> theme_types;
ThemeDB::get_singleton()->get_project_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
ThemeDB::get_singleton()->get_native_type_dependencies(get_class_name(), &theme_types);
ThemeContext *global_context = ThemeDB::get_singleton()->get_default_theme_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_null()) {
continue;
}
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
return ThemeDB::get_singleton()->get_project_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
if (theme->has_font(theme_name, E)) {
return theme->get_font(theme_name, E);
}
}
}
// Lastly, fall back on the items defined in the default Theme, if they exist.
{
List<StringName> theme_types;
ThemeDB::get_singleton()->get_default_theme()->get_type_dependencies(get_class_name(), StringName(), &theme_types);
for (const StringName &E : theme_types) {
if (ThemeDB::get_singleton()->get_default_theme()->has_theme_item(Theme::DATA_TYPE_FONT, "font", E)) {
return ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", E);
}
}
}
// If they don't exist, use any type to return the default/empty value.
return ThemeDB::get_singleton()->get_default_theme()->get_theme_item(Theme::DATA_TYPE_FONT, "font", StringName());
return global_context->get_fallback_theme()->get_font(theme_name, StringName());
}
void TextMesh::set_font_size(int p_size) {

View File

@ -1263,11 +1263,7 @@ void Theme::get_type_dependencies(const StringName &p_base_type, const StringNam
}
// Continue building the chain using native class hierarchy.
StringName class_name = p_base_type;
while (class_name != StringName()) {
p_list->push_back(class_name);
class_name = ClassDB::get_parent_class_nocheck(class_name);
}
ThemeDB::get_singleton()->get_native_type_dependencies(p_base_type, p_list);
}
// Internal methods for getting lists as a Vector of String (compatible with public API).

View File

@ -32,6 +32,9 @@
#include "core/config/project_settings.h"
#include "core/io/resource_loader.h"
#include "scene/gui/control.h"
#include "scene/main/node.h"
#include "scene/main/window.h"
#include "scene/resources/font.h"
#include "scene/resources/style_box.h"
#include "scene/resources/texture.h"
@ -40,18 +43,18 @@
#include "servers/text_server.h"
// Default engine theme creation and configuration.
void ThemeDB::initialize_theme() {
// Default theme-related project settings.
// Allow creating the default theme at a different scale to suit higher/lower base resolutions.
float default_theme_scale = GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "gui/theme/default_theme_scale", PROPERTY_HINT_RANGE, "0.5,8,0.01", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), 1.0);
String theme_path = GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "gui/theme/custom", PROPERTY_HINT_FILE, "*.tres,*.res,*.theme", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), "");
String font_path = GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "gui/theme/custom_font", PROPERTY_HINT_FILE, "*.tres,*.res,*.otf,*.ttf,*.woff,*.woff2,*.fnt,*.font,*.pfb,*.pfm", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), "");
String project_theme_path = GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "gui/theme/custom", PROPERTY_HINT_FILE, "*.tres,*.res,*.theme", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), "");
String project_font_path = GLOBAL_DEF_RST(PropertyInfo(Variant::STRING, "gui/theme/custom_font", PROPERTY_HINT_FILE, "*.tres,*.res,*.otf,*.ttf,*.woff,*.woff2,*.fnt,*.font,*.pfb,*.pfm", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), "");
TextServer::FontAntialiasing font_antialiasing = (TextServer::FontAntialiasing)(int)GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "gui/theme/default_font_antialiasing", PROPERTY_HINT_ENUM, "None,Grayscale,LCD Subpixel", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), 1);
TextServer::Hinting font_hinting = (TextServer::Hinting)(int)GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "gui/theme/default_font_hinting", PROPERTY_HINT_ENUM, "None,Light,Normal", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), TextServer::HINTING_LIGHT);
TextServer::SubpixelPositioning font_subpixel_positioning = (TextServer::SubpixelPositioning)(int)GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "gui/theme/default_font_subpixel_positioning", PROPERTY_HINT_ENUM, "Disabled,Auto,One Half of a Pixel,One Quarter of a Pixel", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED), TextServer::SUBPIXEL_POSITIONING_AUTO);
const bool font_msdf = GLOBAL_DEF_RST("gui/theme/default_font_multichannel_signed_distance_field", false);
@ -60,35 +63,42 @@ void ThemeDB::initialize_theme() {
GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "gui/theme/lcd_subpixel_layout", PROPERTY_HINT_ENUM, "Disabled,Horizontal RGB,Horizontal BGR,Vertical RGB,Vertical BGR"), 1);
ProjectSettings::get_singleton()->set_restart_if_changed("gui/theme/lcd_subpixel_layout", false);
Ref<Font> font;
if (!font_path.is_empty()) {
font = ResourceLoader::load(font_path);
if (font.is_valid()) {
set_fallback_font(font);
} else {
ERR_PRINT("Error loading custom font '" + font_path + "'");
}
}
// Attempt to load custom project theme and font.
// Always make the default theme to avoid invalid default font/icon/style in the given theme.
if (RenderingServer::get_singleton()) {
make_default_theme(default_theme_scale, font, font_subpixel_positioning, font_hinting, font_antialiasing, font_msdf, font_generate_mipmaps);
}
if (!theme_path.is_empty()) {
Ref<Theme> theme = ResourceLoader::load(theme_path);
if (!project_theme_path.is_empty()) {
Ref<Theme> theme = ResourceLoader::load(project_theme_path);
if (theme.is_valid()) {
set_project_theme(theme);
} else {
ERR_PRINT("Error loading custom theme '" + theme_path + "'");
ERR_PRINT("Error loading custom project theme '" + project_theme_path + "'");
}
}
Ref<Font> project_font;
if (!project_font_path.is_empty()) {
project_font = ResourceLoader::load(project_font_path);
if (project_font.is_valid()) {
set_fallback_font(project_font);
} else {
ERR_PRINT("Error loading custom project font '" + project_font_path + "'");
}
}
// Always generate the default theme to serve as a fallback for all required theme definitions.
if (RenderingServer::get_singleton()) {
make_default_theme(default_theme_scale, project_font, font_subpixel_positioning, font_hinting, font_antialiasing, font_msdf, font_generate_mipmaps);
}
_init_default_theme_context();
}
void ThemeDB::initialize_theme_noproject() {
if (RenderingServer::get_singleton()) {
make_default_theme(1.0, Ref<Font>());
}
_init_default_theme_context();
}
void ThemeDB::finalize_theme() {
@ -96,6 +106,7 @@ void ThemeDB::finalize_theme() {
WARN_PRINT("Finalizing theme when there is no RenderingServer is an error; check the order of operations.");
}
_finalize_theme_contexts();
default_theme.unref();
fallback_font.unref();
@ -103,7 +114,7 @@ void ThemeDB::finalize_theme() {
fallback_stylebox.unref();
}
// Universal fallback Theme resources.
// Global Theme resources.
void ThemeDB::set_default_theme(const Ref<Theme> &p_default) {
default_theme = p_default;
@ -188,7 +199,117 @@ Ref<StyleBox> ThemeDB::get_fallback_stylebox() {
return fallback_stylebox;
}
void ThemeDB::get_native_type_dependencies(const StringName &p_base_type, List<StringName> *p_list) {
ERR_FAIL_NULL(p_list);
// TODO: It may make sense to stop at Control/Window, because their parent classes cannot be used in
// a meaningful way.
StringName class_name = p_base_type;
while (class_name != StringName()) {
p_list->push_back(class_name);
class_name = ClassDB::get_parent_class_nocheck(class_name);
}
}
// Global theme contexts.
ThemeContext *ThemeDB::create_theme_context(Node *p_node, List<Ref<Theme>> &p_themes) {
ERR_FAIL_COND_V(!p_node->is_inside_tree(), nullptr);
ERR_FAIL_COND_V(theme_contexts.has(p_node), nullptr);
ERR_FAIL_COND_V(p_themes.is_empty(), nullptr);
ThemeContext *context = memnew(ThemeContext);
context->node = p_node;
context->parent = get_nearest_theme_context(p_node);
context->set_themes(p_themes);
theme_contexts[p_node] = context;
_propagate_theme_context(p_node, context);
p_node->connect("tree_exited", callable_mp(this, &ThemeDB::destroy_theme_context).bind(p_node));
return context;
}
void ThemeDB::destroy_theme_context(Node *p_node) {
ERR_FAIL_COND(!theme_contexts.has(p_node));
p_node->disconnect("tree_exited", callable_mp(this, &ThemeDB::destroy_theme_context));
ThemeContext *context = theme_contexts[p_node];
theme_contexts.erase(p_node);
_propagate_theme_context(p_node, context->parent);
memdelete(context);
}
void ThemeDB::_propagate_theme_context(Node *p_from_node, ThemeContext *p_context) {
Control *from_control = Object::cast_to<Control>(p_from_node);
Window *from_window = from_control ? nullptr : Object::cast_to<Window>(p_from_node);
if (from_control) {
from_control->set_theme_context(p_context);
} else if (from_window) {
from_window->set_theme_context(p_context);
}
for (int i = 0; i < p_from_node->get_child_count(); i++) {
Node *child_node = p_from_node->get_child(i);
// If the child is the root of another global context, stop the propagation
// in this branch.
if (theme_contexts.has(child_node)) {
theme_contexts[child_node]->parent = p_context;
continue;
}
_propagate_theme_context(child_node, p_context);
}
}
void ThemeDB::_init_default_theme_context() {
default_theme_context = memnew(ThemeContext);
List<Ref<Theme>> themes;
themes.push_back(project_theme);
themes.push_back(default_theme);
default_theme_context->set_themes(themes);
}
void ThemeDB::_finalize_theme_contexts() {
if (default_theme_context) {
memdelete(default_theme_context);
default_theme_context = nullptr;
}
while (theme_contexts.size()) {
HashMap<Node *, ThemeContext *>::Iterator E = theme_contexts.begin();
memdelete(E->value);
theme_contexts.remove(E);
}
}
ThemeContext *ThemeDB::get_default_theme_context() const {
return default_theme_context;
}
ThemeContext *ThemeDB::get_nearest_theme_context(Node *p_for_node) const {
ERR_FAIL_COND_V(!p_for_node->is_inside_tree(), nullptr);
Node *parent_node = p_for_node->get_parent();
while (parent_node) {
if (theme_contexts.has(parent_node)) {
return theme_contexts[parent_node];
}
parent_node = parent_node->get_parent();
}
return nullptr;
}
// Object methods.
void ThemeDB::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_default_theme"), &ThemeDB::get_default_theme);
ClassDB::bind_method(D_METHOD("get_project_theme"), &ThemeDB::get_project_theme);
@ -214,7 +335,8 @@ void ThemeDB::_bind_methods() {
ADD_SIGNAL(MethodInfo("fallback_changed"));
}
// Memory management, reference, and initialization
// Memory management, reference, and initialization.
ThemeDB *ThemeDB::singleton = nullptr;
ThemeDB *ThemeDB::get_singleton() {
@ -223,13 +345,15 @@ ThemeDB *ThemeDB::get_singleton() {
ThemeDB::ThemeDB() {
singleton = this;
// Universal default values, final fallback for every theme.
fallback_base_scale = 1.0;
fallback_font_size = 16;
}
ThemeDB::~ThemeDB() {
// For technical reasons unit tests recreate and destroy the default
// theme over and over again. Make sure that finalize_theme() also
// frees any objects that can be recreated by initialize_theme*().
_finalize_theme_contexts();
default_theme.unref();
project_theme.unref();
@ -239,3 +363,43 @@ ThemeDB::~ThemeDB() {
singleton = nullptr;
}
void ThemeContext::_emit_changed() {
emit_signal(SNAME("changed"));
}
void ThemeContext::set_themes(List<Ref<Theme>> &p_themes) {
for (const Ref<Theme> &theme : themes) {
theme->disconnect_changed(callable_mp(this, &ThemeContext::_emit_changed));
}
themes.clear();
for (const Ref<Theme> &theme : p_themes) {
if (theme.is_null()) {
continue;
}
themes.push_back(theme);
theme->connect_changed(callable_mp(this, &ThemeContext::_emit_changed));
}
_emit_changed();
}
List<Ref<Theme>> ThemeContext::get_themes() const {
return themes;
}
Ref<Theme> ThemeContext::get_fallback_theme() const {
// We expect all contexts to be valid and non-empty, but just in case...
if (themes.size() == 0) {
return ThemeDB::get_singleton()->get_default_theme();
}
return themes.back()->get();
}
void ThemeContext::_bind_methods() {
ADD_SIGNAL(MethodInfo("changed"));
}

View File

@ -35,26 +35,39 @@
#include "core/object/ref_counted.h"
class Font;
class Node;
class StyleBox;
class Texture2D;
class Theme;
class ThemeContext;
class ThemeDB : public Object {
GDCLASS(ThemeDB, Object);
static ThemeDB *singleton;
// Universal Theme resources used when no other theme has the item.
// Global Theme resources used by the default theme context.
Ref<Theme> default_theme;
Ref<Theme> project_theme;
// Universal default values, final fallback for every theme.
float fallback_base_scale;
float fallback_base_scale = 1.0;
Ref<Font> fallback_font;
int fallback_font_size;
int fallback_font_size = 16;
Ref<Texture2D> fallback_icon;
Ref<StyleBox> fallback_stylebox;
// Global theme contexts used to scope global Theme resources.
ThemeContext *default_theme_context = nullptr;
HashMap<Node *, ThemeContext *> theme_contexts;
void _propagate_theme_context(Node *p_from_node, ThemeContext *p_context);
void _init_default_theme_context();
void _finalize_theme_contexts();
protected:
static void _bind_methods();
@ -63,7 +76,7 @@ public:
void initialize_theme_noproject();
void finalize_theme();
// Universal Theme resources
// Global Theme resources.
void set_default_theme(const Ref<Theme> &p_default);
Ref<Theme> get_default_theme();
@ -71,7 +84,7 @@ public:
void set_project_theme(const Ref<Theme> &p_project_default);
Ref<Theme> get_project_theme();
// Universal default values.
// Universal fallback values.
void set_fallback_base_scale(float p_base_scale);
float get_fallback_base_scale();
@ -88,9 +101,45 @@ public:
void set_fallback_stylebox(const Ref<StyleBox> &p_stylebox);
Ref<StyleBox> get_fallback_stylebox();
void get_native_type_dependencies(const StringName &p_base_type, List<StringName> *p_list);
// Global theme contexts.
ThemeContext *create_theme_context(Node *p_node, List<Ref<Theme>> &p_themes);
void destroy_theme_context(Node *p_node);
ThemeContext *get_default_theme_context() const;
ThemeContext *get_nearest_theme_context(Node *p_for_node) const;
// Memory management, reference, and initialization.
static ThemeDB *get_singleton();
ThemeDB();
~ThemeDB();
};
class ThemeContext : public Object {
GDCLASS(ThemeContext, Object);
friend class ThemeDB;
Node *node = nullptr;
ThemeContext *parent = nullptr;
// Themes are stacked in the order of relevance, for easy iteration.
// This means that the first theme is the one you should check first,
// and the last theme is the fallback theme where every lookup ends.
List<Ref<Theme>> themes;
void _emit_changed();
protected:
static void _bind_methods();
public:
void set_themes(List<Ref<Theme>> &p_themes);
List<Ref<Theme>> get_themes() const;
Ref<Theme> get_fallback_theme() const;
};
#endif // THEME_DB_H

View File

@ -66,6 +66,52 @@ bool ThemeOwner::has_owner_node() const {
return bool(owner_control || owner_window);
}
void ThemeOwner::set_owner_context(ThemeContext *p_context, bool p_propagate) {
ThemeContext *default_context = ThemeDB::get_singleton()->get_default_theme_context();
if (owner_context && owner_context->is_connected("changed", callable_mp(this, &ThemeOwner::_owner_context_changed))) {
owner_context->disconnect("changed", callable_mp(this, &ThemeOwner::_owner_context_changed));
} else if (default_context->is_connected("changed", callable_mp(this, &ThemeOwner::_owner_context_changed))) {
default_context->disconnect("changed", callable_mp(this, &ThemeOwner::_owner_context_changed));
}
owner_context = p_context;
if (owner_context) {
owner_context->connect("changed", callable_mp(this, &ThemeOwner::_owner_context_changed));
} else {
default_context->connect("changed", callable_mp(this, &ThemeOwner::_owner_context_changed));
}
if (p_propagate) {
_owner_context_changed();
}
}
void ThemeOwner::_owner_context_changed() {
if (!holder->is_inside_tree()) {
// We ignore theme changes outside of tree, because NOTIFICATION_ENTER_TREE covers everything.
return;
}
Control *c = Object::cast_to<Control>(holder);
Window *w = c == nullptr ? Object::cast_to<Window>(holder) : nullptr;
if (c) {
c->notification(Control::NOTIFICATION_THEME_CHANGED);
} else if (w) {
w->notification(Window::NOTIFICATION_THEME_CHANGED);
}
}
ThemeContext *ThemeOwner::_get_active_owner_context() const {
if (owner_context) {
return owner_context;
}
return ThemeDB::get_singleton()->get_default_theme_context();
}
// Theme propagation.
void ThemeOwner::assign_theme_on_parented(Node *p_for_node) {
@ -158,9 +204,7 @@ void ThemeOwner::get_theme_type_dependencies(const Node *p_for_node, const Strin
const Window *for_w = Object::cast_to<Window>(p_for_node);
ERR_FAIL_COND_MSG(!for_c && !for_w, "Only Control and Window nodes and derivatives can be polled for theming.");
Ref<Theme> default_theme = ThemeDB::get_singleton()->get_default_theme();
Ref<Theme> project_theme = ThemeDB::get_singleton()->get_project_theme();
StringName type_name = p_for_node->get_class_name();
StringName type_variation;
if (for_c) {
type_variation = for_c->get_theme_type_variation();
@ -168,31 +212,23 @@ void ThemeOwner::get_theme_type_dependencies(const Node *p_for_node, const Strin
type_variation = for_w->get_theme_type_variation();
}
if (p_theme_type == StringName() || p_theme_type == p_for_node->get_class_name() || p_theme_type == type_variation) {
if (project_theme.is_valid() && project_theme->get_type_variation_base(type_variation) != StringName()) {
project_theme->get_type_dependencies(p_for_node->get_class_name(), type_variation, r_list);
} else {
default_theme->get_type_dependencies(p_for_node->get_class_name(), type_variation, r_list);
}
} else {
default_theme->get_type_dependencies(p_theme_type, StringName(), r_list);
// If we are looking for dependencies of the current class (or a variantion of it), check themes from the context.
if (p_theme_type == StringName() || p_theme_type == type_name || p_theme_type == type_variation) {
ThemeContext *global_context = _get_active_owner_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_valid() && theme->get_type_variation_base(type_variation) != StringName()) {
theme->get_type_dependencies(type_name, type_variation, r_list);
return;
}
}
Node *ThemeOwner::_get_next_owner_node(Node *p_from_node) const {
Node *parent = p_from_node->get_parent();
Control *parent_c = Object::cast_to<Control>(parent);
if (parent_c) {
return parent_c->get_theme_owner_node();
} else {
Window *parent_w = Object::cast_to<Window>(parent);
if (parent_w) {
return parent_w->get_theme_owner_node();
}
// If nothing was found, get the native dependencies for the current class.
ThemeDB::get_singleton()->get_native_type_dependencies(type_name, r_list);
return;
}
return nullptr;
// Otherwise, get the native dependencies for the provided theme type.
ThemeDB::get_singleton()->get_native_type_dependencies(p_theme_type, r_list);
}
Variant ThemeOwner::get_theme_item_in_types(Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types) {
@ -215,24 +251,20 @@ Variant ThemeOwner::get_theme_item_in_types(Theme::DataType p_data_type, const S
owner_node = _get_next_owner_node(owner_node);
}
// Secondly, check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
// Second, check global themes from the appropriate context.
ThemeContext *global_context = _get_active_owner_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_valid()) {
for (const StringName &E : p_theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(p_data_type, p_name, E)) {
return ThemeDB::get_singleton()->get_project_theme()->get_theme_item(p_data_type, p_name, E);
if (theme->has_theme_item(p_data_type, p_name, E)) {
return theme->get_theme_item(p_data_type, p_name, E);
}
}
}
}
// Lastly, fall back on the items defined in the default Theme, if they exist.
for (const StringName &E : p_theme_types) {
if (ThemeDB::get_singleton()->get_default_theme()->has_theme_item(p_data_type, p_name, E)) {
return ThemeDB::get_singleton()->get_default_theme()->get_theme_item(p_data_type, p_name, E);
}
}
// If they don't exist, use any type to return the default/empty value.
return ThemeDB::get_singleton()->get_default_theme()->get_theme_item(p_data_type, p_name, p_theme_types[0]);
// Finally, if no match exists, use any type to return the default/empty value.
return global_context->get_fallback_theme()->get_theme_item(p_data_type, p_name, StringName());
}
bool ThemeOwner::has_theme_item_in_types(Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types) {
@ -255,22 +287,19 @@ bool ThemeOwner::has_theme_item_in_types(Theme::DataType p_data_type, const Stri
owner_node = _get_next_owner_node(owner_node);
}
// Secondly, check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
// Second, check global themes from the appropriate context.
ThemeContext *global_context = _get_active_owner_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_valid()) {
for (const StringName &E : p_theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(p_data_type, p_name, E)) {
if (theme->has_theme_item(p_data_type, p_name, E)) {
return true;
}
}
}
// Lastly, fall back on the items defined in the default Theme, if they exist.
for (const StringName &E : p_theme_types) {
if (ThemeDB::get_singleton()->get_default_theme()->has_theme_item(p_data_type, p_name, E)) {
return true;
}
}
// Finally, if no match exists, return false.
return false;
}
@ -290,17 +319,17 @@ float ThemeOwner::get_theme_default_base_scale() {
owner_node = _get_next_owner_node(owner_node);
}
// Secondly, check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
if (ThemeDB::get_singleton()->get_project_theme()->has_default_base_scale()) {
return ThemeDB::get_singleton()->get_project_theme()->get_default_base_scale();
// Second, check global themes from the appropriate context.
ThemeContext *global_context = _get_active_owner_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_valid()) {
if (theme->has_default_base_scale()) {
return theme->get_default_base_scale();
}
}
}
// Lastly, fall back on the default Theme.
if (ThemeDB::get_singleton()->get_default_theme()->has_default_base_scale()) {
return ThemeDB::get_singleton()->get_default_theme()->get_default_base_scale();
}
// Finally, if no match exists, return the universal default.
return ThemeDB::get_singleton()->get_fallback_base_scale();
}
@ -320,17 +349,17 @@ Ref<Font> ThemeOwner::get_theme_default_font() {
owner_node = _get_next_owner_node(owner_node);
}
// Secondly, check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
if (ThemeDB::get_singleton()->get_project_theme()->has_default_font()) {
return ThemeDB::get_singleton()->get_project_theme()->get_default_font();
// Second, check global themes from the appropriate context.
ThemeContext *global_context = _get_active_owner_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_valid()) {
if (theme->has_default_font()) {
return theme->get_default_font();
}
}
}
// Lastly, fall back on the default Theme.
if (ThemeDB::get_singleton()->get_default_theme()->has_default_font()) {
return ThemeDB::get_singleton()->get_default_theme()->get_default_font();
}
// Finally, if no match exists, return the universal default.
return ThemeDB::get_singleton()->get_fallback_font();
}
@ -350,17 +379,17 @@ int ThemeOwner::get_theme_default_font_size() {
owner_node = _get_next_owner_node(owner_node);
}
// Secondly, check the project-defined Theme resource.
if (ThemeDB::get_singleton()->get_project_theme().is_valid()) {
if (ThemeDB::get_singleton()->get_project_theme()->has_default_font_size()) {
return ThemeDB::get_singleton()->get_project_theme()->get_default_font_size();
// Second, check global themes from the appropriate context.
ThemeContext *global_context = _get_active_owner_context();
for (const Ref<Theme> &theme : global_context->get_themes()) {
if (theme.is_valid()) {
if (theme->has_default_font_size()) {
return theme->get_default_font_size();
}
}
}
// Lastly, fall back on the default Theme.
if (ThemeDB::get_singleton()->get_default_theme()->has_default_font_size()) {
return ThemeDB::get_singleton()->get_default_theme()->get_default_font_size();
}
// Finally, if no match exists, return the universal default.
return ThemeDB::get_singleton()->get_fallback_font_size();
}
@ -377,3 +406,19 @@ Ref<Theme> ThemeOwner::_get_owner_node_theme(Node *p_owner_node) const {
return Ref<Theme>();
}
Node *ThemeOwner::_get_next_owner_node(Node *p_from_node) const {
Node *parent = p_from_node->get_parent();
Control *parent_c = Object::cast_to<Control>(parent);
if (parent_c) {
return parent_c->get_theme_owner_node();
} else {
Window *parent_w = Object::cast_to<Window>(parent);
if (parent_w) {
return parent_w->get_theme_owner_node();
}
}
return nullptr;
}

View File

@ -36,11 +36,18 @@
class Control;
class Node;
class ThemeContext;
class Window;
class ThemeOwner : public Object {
Node *holder = nullptr;
Control *owner_control = nullptr;
Window *owner_window = nullptr;
ThemeContext *owner_context = nullptr;
void _owner_context_changed();
ThemeContext *_get_active_owner_context() const;
Node *_get_next_owner_node(Node *p_from_node) const;
Ref<Theme> _get_owner_node_theme(Node *p_owner_node) const;
@ -52,6 +59,8 @@ public:
Node *get_owner_node() const;
bool has_owner_node() const;
void set_owner_context(ThemeContext *p_context, bool p_propagate = true);
// Theme propagation.
void assign_theme_on_parented(Node *p_for_node);
@ -69,7 +78,7 @@ public:
Ref<Font> get_theme_default_font();
int get_theme_default_font_size();
ThemeOwner() {}
ThemeOwner(Node *p_holder) { holder = p_holder; }
~ThemeOwner() {}
};

View File

@ -238,7 +238,9 @@ struct GodotTestCaseListener : public doctest::IReporter {
RenderingServerDefault::get_singleton()->set_render_loop_enabled(false);
// ThemeDB requires RenderingServer to initialize the default theme.
// So we have to do this for each test case.
// So we have to do this for each test case. Also make sure there is
// no residual theme from something else.
ThemeDB::get_singleton()->finalize_theme();
ThemeDB::get_singleton()->initialize_theme_noproject();
physics_server_3d = PhysicsServer3DManager::get_singleton()->new_default_server();