1 | /**************************************************************************/ |
2 | /* theme_owner.cpp */ |
3 | /**************************************************************************/ |
4 | /* This file is part of: */ |
5 | /* GODOT ENGINE */ |
6 | /* https://godotengine.org */ |
7 | /**************************************************************************/ |
8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
10 | /* */ |
11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
12 | /* a copy of this software and associated documentation files (the */ |
13 | /* "Software"), to deal in the Software without restriction, including */ |
14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
17 | /* the following conditions: */ |
18 | /* */ |
19 | /* The above copyright notice and this permission notice shall be */ |
20 | /* included in all copies or substantial portions of the Software. */ |
21 | /* */ |
22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
29 | /**************************************************************************/ |
30 | |
31 | #include "theme_owner.h" |
32 | |
33 | #include "scene/gui/control.h" |
34 | #include "scene/main/window.h" |
35 | #include "scene/theme/theme_db.h" |
36 | |
37 | // Theme owner node. |
38 | |
39 | void ThemeOwner::set_owner_node(Node *p_node) { |
40 | owner_control = nullptr; |
41 | owner_window = nullptr; |
42 | |
43 | Control *c = Object::cast_to<Control>(p_node); |
44 | if (c) { |
45 | owner_control = c; |
46 | return; |
47 | } |
48 | |
49 | Window *w = Object::cast_to<Window>(p_node); |
50 | if (w) { |
51 | owner_window = w; |
52 | return; |
53 | } |
54 | } |
55 | |
56 | Node *ThemeOwner::get_owner_node() const { |
57 | if (owner_control) { |
58 | return owner_control; |
59 | } else if (owner_window) { |
60 | return owner_window; |
61 | } |
62 | return nullptr; |
63 | } |
64 | |
65 | bool ThemeOwner::has_owner_node() const { |
66 | return bool(owner_control || owner_window); |
67 | } |
68 | |
69 | void ThemeOwner::set_owner_context(ThemeContext *p_context, bool p_propagate) { |
70 | ThemeContext *default_context = ThemeDB::get_singleton()->get_default_theme_context(); |
71 | |
72 | if (owner_context && owner_context->is_connected("changed" , callable_mp(this, &ThemeOwner::_owner_context_changed))) { |
73 | owner_context->disconnect("changed" , callable_mp(this, &ThemeOwner::_owner_context_changed)); |
74 | } else if (default_context->is_connected("changed" , callable_mp(this, &ThemeOwner::_owner_context_changed))) { |
75 | default_context->disconnect("changed" , callable_mp(this, &ThemeOwner::_owner_context_changed)); |
76 | } |
77 | |
78 | owner_context = p_context; |
79 | |
80 | if (owner_context) { |
81 | owner_context->connect("changed" , callable_mp(this, &ThemeOwner::_owner_context_changed)); |
82 | } else { |
83 | default_context->connect("changed" , callable_mp(this, &ThemeOwner::_owner_context_changed)); |
84 | } |
85 | |
86 | if (p_propagate) { |
87 | _owner_context_changed(); |
88 | } |
89 | } |
90 | |
91 | void ThemeOwner::_owner_context_changed() { |
92 | if (!holder->is_inside_tree()) { |
93 | // We ignore theme changes outside of tree, because NOTIFICATION_ENTER_TREE covers everything. |
94 | return; |
95 | } |
96 | |
97 | Control *c = Object::cast_to<Control>(holder); |
98 | Window *w = c == nullptr ? Object::cast_to<Window>(holder) : nullptr; |
99 | |
100 | if (c) { |
101 | c->notification(Control::NOTIFICATION_THEME_CHANGED); |
102 | } else if (w) { |
103 | w->notification(Window::NOTIFICATION_THEME_CHANGED); |
104 | } |
105 | } |
106 | |
107 | ThemeContext *ThemeOwner::_get_active_owner_context() const { |
108 | if (owner_context) { |
109 | return owner_context; |
110 | } |
111 | |
112 | return ThemeDB::get_singleton()->get_default_theme_context(); |
113 | } |
114 | |
115 | // Theme propagation. |
116 | |
117 | void ThemeOwner::assign_theme_on_parented(Node *p_for_node) { |
118 | // We check if there are any themes affecting the parent. If that's the case |
119 | // its children also need to be affected. |
120 | // We don't notify here because `NOTIFICATION_THEME_CHANGED` will be handled |
121 | // a bit later by `NOTIFICATION_ENTER_TREE`. |
122 | |
123 | Node *parent = p_for_node->get_parent(); |
124 | |
125 | Control *parent_c = Object::cast_to<Control>(parent); |
126 | if (parent_c && parent_c->has_theme_owner_node()) { |
127 | propagate_theme_changed(p_for_node, parent_c->get_theme_owner_node(), false, true); |
128 | } else { |
129 | Window *parent_w = Object::cast_to<Window>(parent); |
130 | if (parent_w && parent_w->has_theme_owner_node()) { |
131 | propagate_theme_changed(p_for_node, parent_w->get_theme_owner_node(), false, true); |
132 | } |
133 | } |
134 | } |
135 | |
136 | void ThemeOwner::clear_theme_on_unparented(Node *p_for_node) { |
137 | // We check if there were any themes affecting the parent. If that's the case |
138 | // its children need were also affected and need to be updated. |
139 | // We don't notify because we're exiting the tree, and it's not important. |
140 | |
141 | Node *parent = p_for_node->get_parent(); |
142 | |
143 | Control *parent_c = Object::cast_to<Control>(parent); |
144 | if (parent_c && parent_c->has_theme_owner_node()) { |
145 | propagate_theme_changed(p_for_node, nullptr, false, true); |
146 | } else { |
147 | Window *parent_w = Object::cast_to<Window>(parent); |
148 | if (parent_w && parent_w->has_theme_owner_node()) { |
149 | propagate_theme_changed(p_for_node, nullptr, false, true); |
150 | } |
151 | } |
152 | } |
153 | |
154 | void ThemeOwner::propagate_theme_changed(Node *p_to_node, Node *p_owner_node, bool p_notify, bool p_assign) { |
155 | Control *c = Object::cast_to<Control>(p_to_node); |
156 | Window *w = c == nullptr ? Object::cast_to<Window>(p_to_node) : nullptr; |
157 | |
158 | if (!c && !w) { |
159 | // Theme inheritance chains are broken by nodes that aren't Control or Window. |
160 | return; |
161 | } |
162 | |
163 | bool assign = p_assign; |
164 | if (c) { |
165 | if (c != p_owner_node && c->get_theme().is_valid()) { |
166 | // Has a theme, so we don't want to change the theme owner, |
167 | // but we still want to propagate in case this child has theme items |
168 | // it inherits from the theme this node uses. |
169 | // See https://github.com/godotengine/godot/issues/62844. |
170 | assign = false; |
171 | } |
172 | |
173 | if (assign) { |
174 | c->set_theme_owner_node(p_owner_node); |
175 | } |
176 | |
177 | if (p_notify) { |
178 | c->notification(Control::NOTIFICATION_THEME_CHANGED); |
179 | } |
180 | } else if (w) { |
181 | if (w != p_owner_node && w->get_theme().is_valid()) { |
182 | // Same as above. |
183 | assign = false; |
184 | } |
185 | |
186 | if (assign) { |
187 | w->set_theme_owner_node(p_owner_node); |
188 | } |
189 | |
190 | if (p_notify) { |
191 | w->notification(Window::NOTIFICATION_THEME_CHANGED); |
192 | } |
193 | } |
194 | |
195 | for (int i = 0; i < p_to_node->get_child_count(); i++) { |
196 | propagate_theme_changed(p_to_node->get_child(i), p_owner_node, p_notify, assign); |
197 | } |
198 | } |
199 | |
200 | // Theme lookup. |
201 | |
202 | void ThemeOwner::get_theme_type_dependencies(const Node *p_for_node, const StringName &p_theme_type, List<StringName> *r_list) const { |
203 | const Control *for_c = Object::cast_to<Control>(p_for_node); |
204 | const Window *for_w = Object::cast_to<Window>(p_for_node); |
205 | ERR_FAIL_COND_MSG(!for_c && !for_w, "Only Control and Window nodes and derivatives can be polled for theming." ); |
206 | |
207 | StringName type_name = p_for_node->get_class_name(); |
208 | StringName type_variation; |
209 | if (for_c) { |
210 | type_variation = for_c->get_theme_type_variation(); |
211 | } else if (for_w) { |
212 | type_variation = for_w->get_theme_type_variation(); |
213 | } |
214 | |
215 | // If we are looking for dependencies of the current class (or a variation of it), check themes from the context. |
216 | if (p_theme_type == StringName() || p_theme_type == type_name || p_theme_type == type_variation) { |
217 | ThemeContext *global_context = _get_active_owner_context(); |
218 | for (const Ref<Theme> &theme : global_context->get_themes()) { |
219 | if (theme.is_valid() && theme->get_type_variation_base(type_variation) != StringName()) { |
220 | theme->get_type_dependencies(type_name, type_variation, r_list); |
221 | return; |
222 | } |
223 | } |
224 | |
225 | // If nothing was found, get the native dependencies for the current class. |
226 | ThemeDB::get_singleton()->get_native_type_dependencies(type_name, r_list); |
227 | return; |
228 | } |
229 | |
230 | // Otherwise, get the native dependencies for the provided theme type. |
231 | ThemeDB::get_singleton()->get_native_type_dependencies(p_theme_type, r_list); |
232 | } |
233 | |
234 | Variant ThemeOwner::get_theme_item_in_types(Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types) { |
235 | ERR_FAIL_COND_V_MSG(p_theme_types.size() == 0, Variant(), "At least one theme type must be specified." ); |
236 | |
237 | // First, look through each control or window node in the branch, until no valid parent can be found. |
238 | // Only nodes with a theme resource attached are considered. |
239 | Node *owner_node = get_owner_node(); |
240 | |
241 | while (owner_node) { |
242 | // For each theme resource check the theme types provided and see if p_name exists with any of them. |
243 | for (const StringName &E : p_theme_types) { |
244 | Ref<Theme> owner_theme = _get_owner_node_theme(owner_node); |
245 | |
246 | if (owner_theme.is_valid() && owner_theme->has_theme_item(p_data_type, p_name, E)) { |
247 | return owner_theme->get_theme_item(p_data_type, p_name, E); |
248 | } |
249 | } |
250 | |
251 | owner_node = _get_next_owner_node(owner_node); |
252 | } |
253 | |
254 | // Second, check global themes from the appropriate context. |
255 | ThemeContext *global_context = _get_active_owner_context(); |
256 | for (const Ref<Theme> &theme : global_context->get_themes()) { |
257 | if (theme.is_valid()) { |
258 | for (const StringName &E : p_theme_types) { |
259 | if (theme->has_theme_item(p_data_type, p_name, E)) { |
260 | return theme->get_theme_item(p_data_type, p_name, E); |
261 | } |
262 | } |
263 | } |
264 | } |
265 | |
266 | // Finally, if no match exists, use any type to return the default/empty value. |
267 | return global_context->get_fallback_theme()->get_theme_item(p_data_type, p_name, StringName()); |
268 | } |
269 | |
270 | bool ThemeOwner::has_theme_item_in_types(Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types) { |
271 | ERR_FAIL_COND_V_MSG(p_theme_types.size() == 0, false, "At least one theme type must be specified." ); |
272 | |
273 | // First, look through each control or window node in the branch, until no valid parent can be found. |
274 | // Only nodes with a theme resource attached are considered. |
275 | Node *owner_node = get_owner_node(); |
276 | |
277 | while (owner_node) { |
278 | // For each theme resource check the theme types provided and see if p_name exists with any of them. |
279 | for (const StringName &E : p_theme_types) { |
280 | Ref<Theme> owner_theme = _get_owner_node_theme(owner_node); |
281 | |
282 | if (owner_theme.is_valid() && owner_theme->has_theme_item(p_data_type, p_name, E)) { |
283 | return true; |
284 | } |
285 | } |
286 | |
287 | owner_node = _get_next_owner_node(owner_node); |
288 | } |
289 | |
290 | // Second, check global themes from the appropriate context. |
291 | ThemeContext *global_context = _get_active_owner_context(); |
292 | for (const Ref<Theme> &theme : global_context->get_themes()) { |
293 | if (theme.is_valid()) { |
294 | for (const StringName &E : p_theme_types) { |
295 | if (theme->has_theme_item(p_data_type, p_name, E)) { |
296 | return true; |
297 | } |
298 | } |
299 | } |
300 | } |
301 | |
302 | // Finally, if no match exists, return false. |
303 | return false; |
304 | } |
305 | |
306 | float ThemeOwner::get_theme_default_base_scale() { |
307 | // First, look through each control or window node in the branch, until no valid parent can be found. |
308 | // Only nodes with a theme resource attached are considered. |
309 | // For each theme resource see if their assigned theme has the default value defined and valid. |
310 | Node *owner_node = get_owner_node(); |
311 | |
312 | while (owner_node) { |
313 | Ref<Theme> owner_theme = _get_owner_node_theme(owner_node); |
314 | |
315 | if (owner_theme.is_valid() && owner_theme->has_default_base_scale()) { |
316 | return owner_theme->get_default_base_scale(); |
317 | } |
318 | |
319 | owner_node = _get_next_owner_node(owner_node); |
320 | } |
321 | |
322 | // Second, check global themes from the appropriate context. |
323 | ThemeContext *global_context = _get_active_owner_context(); |
324 | for (const Ref<Theme> &theme : global_context->get_themes()) { |
325 | if (theme.is_valid()) { |
326 | if (theme->has_default_base_scale()) { |
327 | return theme->get_default_base_scale(); |
328 | } |
329 | } |
330 | } |
331 | |
332 | // Finally, if no match exists, return the universal default. |
333 | return ThemeDB::get_singleton()->get_fallback_base_scale(); |
334 | } |
335 | |
336 | Ref<Font> ThemeOwner::get_theme_default_font() { |
337 | // First, look through each control or window node in the branch, until no valid parent can be found. |
338 | // Only nodes with a theme resource attached are considered. |
339 | // For each theme resource see if their assigned theme has the default value defined and valid. |
340 | Node *owner_node = get_owner_node(); |
341 | |
342 | while (owner_node) { |
343 | Ref<Theme> owner_theme = _get_owner_node_theme(owner_node); |
344 | |
345 | if (owner_theme.is_valid() && owner_theme->has_default_font()) { |
346 | return owner_theme->get_default_font(); |
347 | } |
348 | |
349 | owner_node = _get_next_owner_node(owner_node); |
350 | } |
351 | |
352 | // Second, check global themes from the appropriate context. |
353 | ThemeContext *global_context = _get_active_owner_context(); |
354 | for (const Ref<Theme> &theme : global_context->get_themes()) { |
355 | if (theme.is_valid()) { |
356 | if (theme->has_default_font()) { |
357 | return theme->get_default_font(); |
358 | } |
359 | } |
360 | } |
361 | |
362 | // Finally, if no match exists, return the universal default. |
363 | return ThemeDB::get_singleton()->get_fallback_font(); |
364 | } |
365 | |
366 | int ThemeOwner::get_theme_default_font_size() { |
367 | // First, look through each control or window node in the branch, until no valid parent can be found. |
368 | // Only nodes with a theme resource attached are considered. |
369 | // For each theme resource see if their assigned theme has the default value defined and valid. |
370 | Node *owner_node = get_owner_node(); |
371 | |
372 | while (owner_node) { |
373 | Ref<Theme> owner_theme = _get_owner_node_theme(owner_node); |
374 | |
375 | if (owner_theme.is_valid() && owner_theme->has_default_font_size()) { |
376 | return owner_theme->get_default_font_size(); |
377 | } |
378 | |
379 | owner_node = _get_next_owner_node(owner_node); |
380 | } |
381 | |
382 | // Second, check global themes from the appropriate context. |
383 | ThemeContext *global_context = _get_active_owner_context(); |
384 | for (const Ref<Theme> &theme : global_context->get_themes()) { |
385 | if (theme.is_valid()) { |
386 | if (theme->has_default_font_size()) { |
387 | return theme->get_default_font_size(); |
388 | } |
389 | } |
390 | } |
391 | |
392 | // Finally, if no match exists, return the universal default. |
393 | return ThemeDB::get_singleton()->get_fallback_font_size(); |
394 | } |
395 | |
396 | Ref<Theme> ThemeOwner::_get_owner_node_theme(Node *p_owner_node) const { |
397 | const Control *owner_c = Object::cast_to<Control>(p_owner_node); |
398 | if (owner_c) { |
399 | return owner_c->get_theme(); |
400 | } |
401 | |
402 | const Window *owner_w = Object::cast_to<Window>(p_owner_node); |
403 | if (owner_w) { |
404 | return owner_w->get_theme(); |
405 | } |
406 | |
407 | return Ref<Theme>(); |
408 | } |
409 | |
410 | Node *ThemeOwner::_get_next_owner_node(Node *p_from_node) const { |
411 | Node *parent = p_from_node->get_parent(); |
412 | |
413 | Control *parent_c = Object::cast_to<Control>(parent); |
414 | if (parent_c) { |
415 | return parent_c->get_theme_owner_node(); |
416 | } else { |
417 | Window *parent_w = Object::cast_to<Window>(parent); |
418 | if (parent_w) { |
419 | return parent_w->get_theme_owner_node(); |
420 | } |
421 | } |
422 | |
423 | return nullptr; |
424 | } |
425 | |