Skip to content

Commit 482864c

Browse files
committed
Added ability to get a list of project settings changed.
1 parent 6e4e807 commit 482864c

File tree

4 files changed

+224
-2
lines changed

4 files changed

+224
-2
lines changed

core/config/project_settings.cpp

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ String ProjectSettings::globalize_path(const String &p_path) const {
277277
bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
278278
_THREAD_SAFE_METHOD_
279279

280+
// Capture old value before making changes
281+
Variant old_value;
282+
if (props.has(p_name)) {
283+
old_value = props[p_name].variant;
284+
}
285+
280286
if (p_value.get_type() == Variant::NIL) {
281287
props.erase(p_name);
282288
if (p_name.operator String().begins_with("autoload/")) {
@@ -298,7 +304,10 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
298304
}
299305

300306
_version++;
301-
_queue_changed();
307+
308+
if (old_value != p_value) {
309+
_queue_changed(p_name);
310+
}
302311
return true;
303312
}
304313

@@ -344,7 +353,10 @@ bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) {
344353
}
345354

346355
_version++;
347-
_queue_changed();
356+
357+
if (old_value != p_value) {
358+
_queue_changed(p_name);
359+
}
348360
return true;
349361
}
350362

@@ -528,12 +540,31 @@ void ProjectSettings::_queue_changed() {
528540
callable_mp(this, &ProjectSettings::_emit_changed).call_deferred();
529541
}
530542

543+
void ProjectSettings::_queue_changed(const StringName &p_name) {
544+
changed_settings.insert(p_name);
545+
546+
if (!MessageQueue::get_singleton() || MessageQueue::get_singleton()->get_max_buffer_usage() == 0) {
547+
return;
548+
}
549+
550+
// Only queue the deferred call once per frame.
551+
if (!is_changed) {
552+
is_changed = true;
553+
callable_mp(this, &ProjectSettings::_emit_changed).call_deferred();
554+
}
555+
}
556+
531557
void ProjectSettings::_emit_changed() {
532558
if (!is_changed) {
533559
return;
534560
}
535561
is_changed = false;
562+
563+
// Emit the general settings_changed signal to indicate changes are complete.
536564
emit_signal("settings_changed");
565+
566+
// Clear the changed settings after emitting the signal
567+
clear_changed_settings();
537568
}
538569

539570
bool ProjectSettings::load_resource_pack(const String &p_pack, bool p_replace_files, int p_offset) {
@@ -1313,6 +1344,31 @@ Variant ProjectSettings::get_setting(const String &p_setting, const Variant &p_d
13131344
}
13141345
}
13151346

1347+
PackedStringArray ProjectSettings::get_changed_settings() const {
1348+
PackedStringArray arr;
1349+
for (const String &setting : changed_settings) {
1350+
arr.push_back(setting);
1351+
}
1352+
return arr;
1353+
}
1354+
1355+
bool ProjectSettings::check_changed_settings_in_group(const String &p_setting_prefix) const {
1356+
for (const String &setting : changed_settings) {
1357+
if (setting.begins_with(p_setting_prefix)) {
1358+
return true;
1359+
}
1360+
}
1361+
return false;
1362+
}
1363+
1364+
void ProjectSettings::mark_setting_changed(const String &p_setting) {
1365+
changed_settings.insert(p_setting);
1366+
}
1367+
1368+
void ProjectSettings::clear_changed_settings() {
1369+
changed_settings.clear();
1370+
}
1371+
13161372
void ProjectSettings::refresh_global_class_list() {
13171373
// This is called after mounting a new PCK file to pick up class changes.
13181374
is_global_class_list_loaded = false; // Make sure we read from the freshly mounted PCK.
@@ -1509,6 +1565,12 @@ void ProjectSettings::_bind_methods() {
15091565

15101566
ClassDB::bind_method(D_METHOD("save_custom", "file"), &ProjectSettings::_save_custom_bnd);
15111567

1568+
// Change tracking methods
1569+
ClassDB::bind_method(D_METHOD("get_changed_settings"), &ProjectSettings::get_changed_settings);
1570+
ClassDB::bind_method(D_METHOD("check_changed_settings_in_group", "setting_prefix"), &ProjectSettings::check_changed_settings_in_group);
1571+
ClassDB::bind_method(D_METHOD("mark_setting_changed", "setting"), &ProjectSettings::mark_setting_changed);
1572+
ClassDB::bind_method(D_METHOD("clear_changed_settings"), &ProjectSettings::clear_changed_settings);
1573+
15121574
ADD_SIGNAL(MethodInfo("settings_changed"));
15131575
}
15141576

core/config/project_settings.h

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

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

121124
void _queue_changed();
125+
void _queue_changed(const StringName &p_name);
122126
void _emit_changed();
123127

124128
static inline ProjectSettings *singleton = nullptr;
@@ -209,6 +213,12 @@ class ProjectSettings : public Object {
209213

210214
bool has_custom_feature(const String &p_feature) const;
211215

216+
// Change tracking methods
217+
PackedStringArray get_changed_settings() const;
218+
bool check_changed_settings_in_group(const String &p_setting_prefix) const;
219+
void mark_setting_changed(const String &p_setting);
220+
void clear_changed_settings();
221+
212222
const HashMap<StringName, AutoloadInfo> &get_autoload_list() const;
213223
void add_autoload(const AutoloadInfo &p_autoload);
214224
void remove_autoload(const StringName &p_autoload);

doc/classes/ProjectSettings.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,32 @@
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="clear_changed_settings">
72+
<return type="void" />
73+
<description>
74+
Clears the list of changed settings. 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>
77+
<method name="get_changed_settings" qualifiers="const">
78+
<return type="PackedStringArray" />
79+
<description>
80+
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].
81+
</description>
82+
</method>
6483
<method name="get_global_class_list">
6584
<return type="Dictionary[]" />
6685
<description>
@@ -172,6 +191,13 @@
172191
Returns the localized path (starting with [code]res://[/code]) corresponding to the absolute, native OS [param path]. See also [method globalize_path].
173192
</description>
174193
</method>
194+
<method name="mark_setting_changed">
195+
<return type="void" />
196+
<param index="0" name="setting" type="String" />
197+
<description>
198+
Marks the passed project setting as being changed, see [method get_changed_settings]. Only settings which exist (see [method has_setting]) will be accepted.
199+
</description>
200+
</method>
175201
<method name="save">
176202
<return type="int" enum="Error" />
177203
<description>

tests/core/config/test_project_settings.h

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,128 @@ 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+
ProjectSettings::get_singleton()->clear_changed_settings();
171+
172+
PackedStringArray initial_changes = ProjectSettings::get_singleton()->get_changed_settings();
173+
CHECK_EQ(initial_changes.size(), 0);
174+
175+
String setting_name = "test_changed_setting";
176+
ProjectSettings::get_singleton()->set_setting(setting_name, "test_value");
177+
178+
PackedStringArray changes_after_set = ProjectSettings::get_singleton()->get_changed_settings();
179+
CHECK_EQ(changes_after_set.size(), 1);
180+
CHECK_EQ(changes_after_set[0], setting_name);
181+
}
182+
183+
TEST_CASE("[ProjectSettings] get_changed_settings multiple settings") {
184+
ProjectSettings::get_singleton()->clear_changed_settings();
185+
186+
ProjectSettings::get_singleton()->set_setting("test_setting_1", "value1");
187+
ProjectSettings::get_singleton()->set_setting("test_setting_2", "value2");
188+
ProjectSettings::get_singleton()->set_setting("another_group/setting", "value3");
189+
190+
PackedStringArray changes = ProjectSettings::get_singleton()->get_changed_settings();
191+
CHECK_EQ(changes.size(), 3);
192+
193+
CHECK(changes.has("test_setting_1"));
194+
CHECK(changes.has("test_setting_2"));
195+
CHECK(changes.has("another_group/setting"));
196+
}
197+
198+
TEST_CASE("[ProjectSettings] check_changed_settings_in_group") {
199+
ProjectSettings::get_singleton()->clear_changed_settings();
200+
201+
ProjectSettings::get_singleton()->set_setting("group1/setting1", "value1");
202+
ProjectSettings::get_singleton()->set_setting("group1/setting2", "value2");
203+
ProjectSettings::get_singleton()->set_setting("group2/setting1", "value3");
204+
ProjectSettings::get_singleton()->set_setting("other_setting", "value4");
205+
206+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group1/"));
207+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group2/"));
208+
CHECK_FALSE(ProjectSettings::get_singleton()->check_changed_settings_in_group("nonexistent/"));
209+
210+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("group"));
211+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("other"));
212+
}
213+
214+
TEST_CASE("[ProjectSettings] clear_changed_settings") {
215+
ProjectSettings::get_singleton()->set_setting("clear_test_1", "value1");
216+
ProjectSettings::get_singleton()->set_setting("clear_test_2", "value2");
217+
218+
PackedStringArray changes_before = ProjectSettings::get_singleton()->get_changed_settings();
219+
CHECK_GE(changes_before.size(), 2);
220+
221+
ProjectSettings::get_singleton()->clear_changed_settings();
222+
223+
PackedStringArray changes_after = ProjectSettings::get_singleton()->get_changed_settings();
224+
CHECK_EQ(changes_after.size(), 0);
225+
}
226+
227+
TEST_CASE("[ProjectSettings] mark_setting_changed") {
228+
ProjectSettings::get_singleton()->clear_changed_settings();
229+
230+
String setting_name = "manually_marked_setting";
231+
ProjectSettings::get_singleton()->mark_setting_changed(setting_name);
232+
233+
PackedStringArray changes = ProjectSettings::get_singleton()->get_changed_settings();
234+
CHECK_EQ(changes.size(), 1);
235+
CHECK_EQ(changes[0], setting_name);
236+
237+
CHECK(ProjectSettings::get_singleton()->check_changed_settings_in_group("manually_marked"));
238+
}
239+
240+
TEST_CASE("[SceneTree][ProjectSettings] Changes cleared after settings_changed signal") {
241+
SIGNAL_WATCH(ProjectSettings::get_singleton(), SNAME("settings_changed"));
242+
243+
ProjectSettings::get_singleton()->clear_changed_settings();
244+
SIGNAL_DISCARD("settings_changed");
245+
246+
ProjectSettings::get_singleton()->set_setting("signal_clear_test", "value");
247+
248+
PackedStringArray changes_before = ProjectSettings::get_singleton()->get_changed_settings();
249+
CHECK_EQ(changes_before.size(), 1);
250+
CHECK_EQ(changes_before[0], "signal_clear_test");
251+
252+
MessageQueue::get_singleton()->flush();
253+
254+
SIGNAL_CHECK("settings_changed", { {} });
255+
256+
PackedStringArray changes_after = ProjectSettings::get_singleton()->get_changed_settings();
257+
CHECK_EQ(changes_after.size(), 0);
258+
259+
SIGNAL_UNWATCH(ProjectSettings::get_singleton(), SNAME("settings_changed"));
260+
}
261+
262+
TEST_CASE("[ProjectSettings] No tracking when setting same value") {
263+
ProjectSettings::get_singleton()->clear_changed_settings();
264+
265+
String setting_name = "same_value_test";
266+
String test_value = "same_value";
267+
268+
ProjectSettings::get_singleton()->set_setting(setting_name, test_value);
269+
270+
PackedStringArray changes_after_first = ProjectSettings::get_singleton()->get_changed_settings();
271+
CHECK_EQ(changes_after_first.size(), 1);
272+
273+
ProjectSettings::get_singleton()->clear_changed_settings();
274+
275+
// Set the same value again, it should not be tracked.
276+
ProjectSettings::get_singleton()->set_setting(setting_name, test_value);
277+
278+
PackedStringArray changes_after_same = ProjectSettings::get_singleton()->get_changed_settings();
279+
CHECK_EQ(changes_after_same.size(), 0);
280+
}
281+
158282
} // namespace TestProjectSettings

0 commit comments

Comments
 (0)