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

@@ -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"));
}