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

@ -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);
// 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;
}
}
} else {
default_theme->get_type_dependencies(p_theme_type, StringName(), r_list);
}
}
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()) {
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);
// 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 (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()) {
for (const StringName &E : p_theme_types) {
if (ThemeDB::get_singleton()->get_project_theme()->has_theme_item(p_data_type, p_name, E)) {
return true;
// 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 (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;
}