Skip to content

Commit 6fd949a

Browse files
committed
Merge pull request #110748 from MauriceButler/project-setting-changed-signal
Add ability to get list of Project Settings changed, similar to Editor Settings functionality
2 parents 7df702b + ffa2651 commit 6fd949a

File tree

4 files changed

+141
-7
lines changed

4 files changed

+141
-7
lines changed

core/config/project_settings.cpp

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ String ProjectSettings::globalize_path(const String &p_path) const {
283283
bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
284284
_THREAD_SAFE_METHOD_
285285

286+
// Early return if value hasn't changed (unless it's being deleted)
287+
if (p_value.get_type() != Variant::NIL) {
288+
if (props.has(p_name) && props[p_name].variant == p_value) {
289+
return true;
290+
}
291+
}
292+
286293
if (p_value.get_type() == Variant::NIL) {
287294
props.erase(p_name);
288295
if (p_name.operator String().begins_with("autoload/")) {
@@ -304,7 +311,7 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
304311
}
305312

306313
_version++;
307-
_queue_changed();
314+
_queue_changed(p_name);
308315
return true;
309316
}
310317

@@ -350,7 +357,7 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
350357
}
351358

352359
_version++;
353-
_queue_changed();
360+
_queue_changed(p_name);
354361
return true;
355362
}
356363

@@ -526,20 +533,31 @@ void ProjectSettings::_get_property_list(List<PropertyInfo> *p_list) const {
526533
}
527534
}
528535

529-
void ProjectSettings::_queue_changed() {
530-
if (is_changed || !MessageQueue::get_singleton() || MessageQueue::get_singleton()->get_max_buffer_usage() == 0) {
536+
void ProjectSettings::_queue_changed(const StringName &p_name) {
537+
changed_settings.insert(p_name);
538+
539+
if (!MessageQueue::get_singleton() || MessageQueue::get_singleton()->get_max_buffer_usage() == 0) {
531540
return;
532541
}
533-
is_changed = true;
534-
callable_mp(this, &ProjectSettings::_emit_changed).call_deferred();
542+
543+
// Only queue the deferred call once per frame.
544+
if (!is_changed) {
545+
is_changed = true;
546+
callable_mp(this, &ProjectSettings::_emit_changed).call_deferred();
547+
}
535548
}
536549

537550
void ProjectSettings::_emit_changed() {
538551
if (!is_changed) {
539552
return;
540553
}
541554
is_changed = false;
555+
556+
// Emit the general settings_changed signal to indicate changes are complete.
542557
emit_signal("settings_changed");
558+
559+
// Clear the changed settings after emitting the signal
560+
changed_settings.clear();
543561
}
544562

545563
bool ProjectSettings::load_resource_pack(const String &p_pack, bool p_replace_files, int p_offset) {
@@ -1352,6 +1370,23 @@ Variant ProjectSettings::get_setting(const String &p_setting, const Variant &p_d
13521370
}
13531371
}
13541372

1373+
PackedStringArray ProjectSettings::get_changed_settings() const {
1374+
PackedStringArray arr;
1375+
for (const StringName &setting : changed_settings) {
1376+
arr.push_back(setting);
1377+
}
1378+
return arr;
1379+
}
1380+
1381+
bool ProjectSettings::check_changed_settings_in_group(const String &p_setting_prefix) const {
1382+
for (const StringName &setting : changed_settings) {
1383+
if (String(setting).begins_with(p_setting_prefix)) {
1384+
return true;
1385+
}
1386+
}
1387+
return false;
1388+
}
1389+
13551390
void ProjectSettings::refresh_global_class_list() {
13561391
// This is called after mounting a new PCK file to pick up class changes.
13571392
is_global_class_list_loaded = false; // Make sure we read from the freshly mounted PCK.
@@ -1548,6 +1583,9 @@ void ProjectSettings::_bind_methods() {
15481583

15491584
ClassDB::bind_method(D_METHOD("save_custom", "file"), &ProjectSettings::_save_custom_bnd);
15501585

1586+
// Change tracking methods
1587+
ClassDB::bind_method(D_METHOD("get_changed_settings"), &ProjectSettings::get_changed_settings);
1588+
ClassDB::bind_method(D_METHOD("check_changed_settings_in_group", "setting_prefix"), &ProjectSettings::check_changed_settings_in_group);
15511589
ADD_SIGNAL(MethodInfo("settings_changed"));
15521590
}
15531591

core/config/project_settings.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class ProjectSettings : public Object {
4747
// and will always detect the initial project settings as a "change".
4848
uint32_t _version = 1;
4949

50+
// Track changed settings for get_changed_settings functionality
51+
HashSet<StringName> changed_settings;
52+
5053
public:
5154
typedef HashMap<String, Variant> CustomMap;
5255
static inline const String PROJECT_DATA_DIR_NAME_SUFFIX = "godot";
@@ -119,7 +122,7 @@ class ProjectSettings : public Object {
119122
bool _property_can_revert(const StringName &p_name) const;
120123
bool _property_get_revert(const StringName &p_name, Variant &r_property) const;
121124

122-
void _queue_changed();
125+
void _queue_changed(const StringName &p_name);
123126
void _emit_changed();
124127

125128
static inline ProjectSettings *singleton = nullptr;
@@ -210,6 +213,10 @@ class ProjectSettings : public Object {
210213

211214
bool has_custom_feature(const String &p_feature) const;
212215

216+
// Change tracking methods
217+
PackedStringArray get_changed_settings() const;
218+
bool check_changed_settings_in_group(const String &p_setting_prefix) const;
219+
213220
const HashMap<StringName, AutoloadInfo> &get_autoload_list() const;
214221
void add_autoload(const AutoloadInfo &p_autoload);
215222
void remove_autoload(const StringName &p_autoload);

doc/classes/ProjectSettings.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,26 @@
5454
[b]Note:[/b] Setting [code]"usage"[/code] for the property is not supported. Use [method set_as_basic], [method set_restart_if_changed], and [method set_as_internal] to modify usage flags.
5555
</description>
5656
</method>
57+
<method name="check_changed_settings_in_group" qualifiers="const">
58+
<return type="bool" />
59+
<param index="0" name="setting_prefix" type="String" />
60+
<description>
61+
Checks if any settings with the prefix [param setting_prefix] exist in the set of changed settings. See also [method get_changed_settings].
62+
</description>
63+
</method>
5764
<method name="clear">
5865
<return type="void" />
5966
<param index="0" name="name" type="String" />
6067
<description>
6168
Clears the whole configuration (not recommended, may break things).
6269
</description>
6370
</method>
71+
<method name="get_changed_settings" qualifiers="const">
72+
<return type="PackedStringArray" />
73+
<description>
74+
Gets an array of the settings which have been changed since the last save. Note that internally [code]changed_settings[/code] is cleared after a successful save, so generally the most appropriate place to use this method is when processing [signal settings_changed].
75+
</description>
76+
</method>
6477
<method name="get_global_class_list">
6578
<return type="Dictionary[]" />
6679
<description>

tests/core/config/test_project_settings.h

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,80 @@ TEST_CASE("[ProjectSettings] localize_path") {
155155
TestProjectSettingsInternalsAccessor::resource_path() = old_resource_path;
156156
}
157157

158+
TEST_CASE("[SceneTree][ProjectSettings] settings_changed signal") {
159+
SIGNAL_WATCH(ProjectSettings::get_singleton(), SNAME("settings_changed"));
160+
161+
ProjectSettings::get_singleton()->set_setting("test_signal_setting", "test_value");
162+
MessageQueue::get_singleton()->flush();
163+
164+
SIGNAL_CHECK("settings_changed", { {} });
165+
166+
SIGNAL_UNWATCH(ProjectSettings::get_singleton(), SNAME("settings_changed"));
167+
}
168+
169+
TEST_CASE("[ProjectSettings] get_changed_settings basic functionality") {
170+
String setting_name = "test_changed_setting";
171+
ProjectSettings::get_singleton()->set_setting(setting_name, "test_value");
172+
173+
PackedStringArray changes = ProjectSettings::get_singleton()->get_changed_settings();
174+
CHECK(changes.has(setting_name));
175+
}
176+
177+
TEST_CASE("[ProjectSettings] get_changed_settings multiple settings") {
178+
ProjectSettings::get_singleton()->set_setting("test_setting_1", "value1");
179+
ProjectSettings::get_singleton()->set_setting("test_setting_2", "value2");
180+
ProjectSettings::get_singleton()->set_setting("another_group/setting", "value3");
181+
182+
PackedStringArray changes = ProjectSettings::get_singleton()->get_changed_settings();
183+
CHECK(changes.has("test_setting_1"));
184+
CHECK(changes.has("test_setting_2"));
185+
CHECK(changes.has("another_group/setting"));
186+
}
187+
188+
TEST_CASE("[ProjectSettings] check_changed_settings_in_group") {
189+
ProjectSettings::get_singleton()->set_setting("group1/setting1", "value1");
190+
ProjectSettings::get_singleton()->set_setting("group1/setting2", "value2");
191+
ProjectSettings::get_singleton()->set_setting("group2/setting1", "value3");
192+
ProjectSettings::get_singleton()->set_setting("other_setting", "value4");
193+
194+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group1/"));
195+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group2/"));
196+
CHECK_FALSE(ProjectSettings::get_singleton()->check_changed_settings_in_group("nonexistent/"));
197+
198+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group1"));
199+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("other_setting"));
200+
}
201+
202+
TEST_CASE("[SceneTree][ProjectSettings] Changes cleared after settings_changed signal") {
203+
SIGNAL_WATCH(ProjectSettings::get_singleton(), SNAME("settings_changed"));
204+
205+
ProjectSettings::get_singleton()->set_setting("signal_clear_test", "value");
206+
207+
PackedStringArray changes_before = ProjectSettings::get_singleton()->get_changed_settings();
208+
CHECK(changes_before.has("signal_clear_test"));
209+
210+
MessageQueue::get_singleton()->flush();
211+
212+
SIGNAL_CHECK("settings_changed", { {} });
213+
214+
PackedStringArray changes_after = ProjectSettings::get_singleton()->get_changed_settings();
215+
CHECK_FALSE(changes_after.has("signal_clear_test"));
216+
217+
SIGNAL_UNWATCH(ProjectSettings::get_singleton(), SNAME("settings_changed"));
218+
}
219+
220+
TEST_CASE("[ProjectSettings] No tracking when setting same value") {
221+
String setting_name = "same_value_test";
222+
String test_value = "same_value";
223+
224+
ProjectSettings::get_singleton()->set_setting(setting_name, test_value);
225+
int count_before = ProjectSettings::get_singleton()->get_changed_settings().size();
226+
227+
// Setting the same value should not be tracked due to early return.
228+
ProjectSettings::get_singleton()->set_setting(setting_name, test_value);
229+
int count_after = ProjectSettings::get_singleton()->get_changed_settings().size();
230+
231+
CHECK_EQ(count_before, count_after);
232+
}
233+
158234
} // namespace TestProjectSettings

0 commit comments

Comments
 (0)