From f060224cbe1a21d8a41d27552c7838f35092cabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Pe=C3=B1a?= Date: Thu, 16 Jul 2026 19:06:02 +0800 Subject: [PATCH] feat: add option to extract downloaded binaries directly in versions folder. Also adapted the local editor deletion flow to allow only deleting the editor binary instead of the parent folder (current behaviour). --- .../editors/local/editor_item/editor_item.gd | 25 ++++++++----- .../local/editors_list/editors_list.gd | 4 +-- src/components/editors/local/local_editors.gd | 36 ++++++++++++++----- .../editors/remote/remote_editors.gd | 18 +++++++--- src/components/settings/settings_window.gd | 6 ++++ src/config.gd | 15 ++++++-- .../editors/resolve_removal_target_test.gd | 32 +++++++++++++++++ .../resolve_removal_target_test.gd.uid | 1 + tests/cases/editors/resolve_unzip_dir_test.gd | 13 +++++++ .../editors/resolve_unzip_dir_test.gd.uid | 1 + 10 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 tests/cases/editors/resolve_removal_target_test.gd create mode 100644 tests/cases/editors/resolve_removal_target_test.gd.uid create mode 100644 tests/cases/editors/resolve_unzip_dir_test.gd create mode 100644 tests/cases/editors/resolve_unzip_dir_test.gd.uid diff --git a/src/components/editors/local/editor_item/editor_item.gd b/src/components/editors/local/editor_item/editor_item.gd index 959f5b98..f42134f2 100644 --- a/src/components/editors/local/editor_item/editor_item.gd +++ b/src/components/editors/local/editor_item/editor_item.gd @@ -2,7 +2,7 @@ class_name EditorListItemControl extends HBoxListItem signal edited -signal removed(remove_dir: bool) +signal removed(remove_dir: bool, remove_binary: bool) signal manage_tags_requested signal tag_clicked(tag: String) @@ -299,23 +299,30 @@ func _on_remove(item: LocalEditors.Item) -> void: warning.self_modulate = get_theme_color("warning_color", "Editor") warning.hide() - var checkbox := CheckBox.new() - checkbox.text = tr("remove also from the file system") - checkbox.toggled.connect(func(toggled: bool) -> void: + var remove_binary_checkbox := CheckBox.new() + remove_binary_checkbox.text = tr("delete binary from filesystem") + + var remove_dir_checkbox := CheckBox.new() + remove_dir_checkbox.text = tr("delete parent folder in filesystem") + remove_dir_checkbox.toggled.connect(func(toggled: bool) -> void: warning.visible = toggled + if toggled: + remove_binary_checkbox.button_pressed = false + remove_binary_checkbox.disabled = toggled ) - + var vb := VBoxContainer.new() vb.add_child(label) - vb.add_child(checkbox) + vb.add_child(remove_binary_checkbox) + vb.add_child(remove_dir_checkbox) vb.add_child(warning) vb.add_spacer(false) - + confirmation_dialog.add_child(vb) - + confirmation_dialog.confirmed.connect(func() -> void: queue_free() - removed.emit(checkbox.button_pressed) + removed.emit(remove_dir_checkbox.button_pressed, remove_binary_checkbox.button_pressed) ) add_child(confirmation_dialog) confirmation_dialog.popup_centered() diff --git a/src/components/editors/local/editors_list/editors_list.gd b/src/components/editors/local/editors_list/editors_list.gd index 8678d8a7..b521cfde 100644 --- a/src/components/editors/local/editors_list/editors_list.gd +++ b/src/components/editors/local/editors_list/editors_list.gd @@ -1,7 +1,7 @@ class_name EditorsVBoxList extends VBoxList -signal item_removed(item_data: LocalEditors.Item, remove_dir: bool) +signal item_removed(item_data: LocalEditors.Item, remove_dir: bool, remove_binary: bool) signal item_edited(item_data: LocalEditors.Item) signal item_manage_tags_requested(item_data: LocalEditors.Item) @@ -10,7 +10,7 @@ func _post_add(raw_item_data: Object, raw_item_control: Control) -> void: var item_data := raw_item_data as LocalEditors.Item var item_control := raw_item_control as EditorListItemControl item_control.removed.connect( - func(remove_dir: bool) -> void: item_removed.emit(item_data, remove_dir) + func(remove_dir: bool, remove_binary: bool) -> void: item_removed.emit(item_data, remove_dir, remove_binary) ) item_control.edited.connect( func() -> void: item_edited.emit(item_data) diff --git a/src/components/editors/local/local_editors.gd b/src/components/editors/local/local_editors.gd index 2a8208fa..ad407de4 100644 --- a/src/components/editors/local/local_editors.gd +++ b/src/components/editors/local/local_editors.gd @@ -197,17 +197,17 @@ func _on_editors_list_item_selected(item: EditorListItemControl) -> void: _sidebar.refresh_actions(item.get_actions()) -func _on_editors_list_item_removed(item_data: LocalEditors.Item, remove_dir: bool) -> void: +func _on_editors_list_item_removed(item_data: LocalEditors.Item, remove_dir: bool, remove_binary: bool) -> void: + var target := _resolve_removal_target( + item_data.path, Config.VERSIONS_PATH.ret() as String, remove_dir, remove_binary + ) if remove_dir: - var base_dir := ProjectSettings.globalize_path(item_data.path.get_base_dir()) - var versions_dir := ProjectSettings.globalize_path(Config.VERSIONS_PATH.ret() as String) - if not OS.has_feature("linux"): - base_dir = base_dir.to_lower() - versions_dir = versions_dir.to_lower() - if base_dir != versions_dir and base_dir.begins_with(versions_dir): - edir.remove_recursive(base_dir) + if target != "": + edir.remove_recursive(target) else: - Output.push("skipping removing path {%s}" % base_dir) + Output.push("skipping removing path {%s}" % ProjectSettings.globalize_path(item_data.path.get_base_dir())) + elif remove_binary: + DirAccess.remove_absolute(target) if _local_editors.has(item_data.path): _local_editors.erase(item_data.path) _local_editors.save() @@ -215,6 +215,24 @@ func _on_editors_list_item_removed(item_data: LocalEditors.Item, remove_dir: boo _update_remove_missing_disabled() +# Returns the absolute path to delete from the filesystem, or "" if nothing should be +# removed. The parent folder is only removable when it is a sub-folder of the versions +# dir (never the versions dir itself, e.g. for "extract to same folder" installs). +static func _resolve_removal_target(editor_path: String, versions_path: String, remove_dir: bool, remove_binary: bool) -> String: + if remove_dir: + var base_dir := ProjectSettings.globalize_path(editor_path.get_base_dir()) + var versions_dir := ProjectSettings.globalize_path(versions_path) + if not OS.has_feature("linux"): + base_dir = base_dir.to_lower() + versions_dir = versions_dir.to_lower() + if base_dir != versions_dir and base_dir.begins_with(versions_dir): + return base_dir + return "" + if remove_binary: + return ProjectSettings.globalize_path(editor_path) + return "" + + func _on_editors_list_item_edited(item_data: Variant) -> void: _local_editors.save() _editors_list.sort_items() diff --git a/src/components/editors/remote/remote_editors.gd b/src/components/editors/remote/remote_editors.gd index 7b2c4711..8bea2e69 100644 --- a/src/components/editors/remote/remote_editors.gd +++ b/src/components/editors/remote/remote_editors.gd @@ -120,9 +120,19 @@ func install_zip(zip_abs_path: String, root_unzip_folder_name: String, possible_ func _unzip_downloaded(downloaded_abs_path: String, root_unzip_folder_name: String) -> String: - var zip_content_dir := "%s/%s" % [Config.VERSIONS_PATH.ret(), root_unzip_folder_name] - if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(zip_content_dir)): - zip_content_dir += "-%s" % uuid.v4().substr(0, 8) - zip_content_dir += "/" + var zip_content_dir := _resolve_unzip_dir( + Config.VERSIONS_PATH.ret() as String, + root_unzip_folder_name, + Config.EXTRACT_TO_SAME_FOLDER.ret() as bool + ) zip.unzip(downloaded_abs_path, zip_content_dir) return zip_content_dir + + +static func _resolve_unzip_dir(versions_path: String, root_unzip_folder_name: String, extract_to_same_folder: bool) -> String: + if extract_to_same_folder: + return versions_path + "/" + var zip_content_dir := "%s/%s" % [versions_path, root_unzip_folder_name] + if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(zip_content_dir)): + zip_content_dir += "-%s" % uuid.v4().substr(0, 8) + return zip_content_dir + "/" diff --git a/src/components/settings/settings_window.gd b/src/components/settings/settings_window.gd index 07df753d..c8ba99eb 100644 --- a/src/components/settings/settings_window.gd +++ b/src/components/settings/settings_window.gd @@ -138,6 +138,12 @@ func _prepare_settings() -> Array: SettingCheckbox, tr("Will check only stable Godots releases.") )), + SettingChangeObserved(SettingCfg( + "application/advanced/extract_to_same_folder", + Config.EXTRACT_TO_SAME_FOLDER, + SettingCheckbox, + tr("Extract downloaded editors directly into the versions dir instead of creating a subfolder for each one.") + )), SettingChangeObserved(SettingCfg( "network/http_proxy/host", Config.HTTP_PROXY_HOST, diff --git a/src/config.gd b/src/config.gd index eeb7c895..2e3debb2 100644 --- a/src/config.gd +++ b/src/config.gd @@ -181,11 +181,20 @@ var ALLOW_INSTALL_TO_NOT_EMPTY_DIR := ConfigFileValue.new( var ONLY_STABLE_UPDATES := ConfigFileValue.new( - _cfg_auto_save.as_config_like(), - "app", + _cfg_auto_save.as_config_like(), + "app", "only_stable_updates", true -): +): + set(_v): _readonly() + + +var EXTRACT_TO_SAME_FOLDER := ConfigFileValue.new( + _cfg_auto_save.as_config_like(), + "app", + "extract_to_same_folder", + false +): set(_v): _readonly() diff --git a/tests/cases/editors/resolve_removal_target_test.gd b/tests/cases/editors/resolve_removal_target_test.gd new file mode 100644 index 00000000..b4dc4899 --- /dev/null +++ b/tests/cases/editors/resolve_removal_target_test.gd @@ -0,0 +1,32 @@ +extends GdUnitTestSuite + + +func test_delete_parent_folder_returns_the_editor_subfolder() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/Godot_v4.5-stable/godot", "/versions", true, false) + ).is_equal("/versions/Godot_v4.5-stable") + + +func test_delete_parent_folder_is_skipped_for_same_folder_installs() -> void: + # The binary lives directly in the versions dir, so the guard refuses to remove it. + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/godot", "/versions", true, false) + ).is_equal("") + + +func test_delete_parent_folder_is_skipped_outside_the_versions_dir() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/elsewhere/godot", "/versions", true, false) + ).is_equal("") + + +func test_delete_binary_returns_the_binary_path() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/godot", "/versions", false, true) + ).is_equal("/versions/godot") + + +func test_no_option_selected_removes_nothing() -> void: + assert_str( + LocalEditorsControl._resolve_removal_target("/versions/Godot_v4.5-stable/godot", "/versions", false, false) + ).is_equal("") diff --git a/tests/cases/editors/resolve_removal_target_test.gd.uid b/tests/cases/editors/resolve_removal_target_test.gd.uid new file mode 100644 index 00000000..90c4e67f --- /dev/null +++ b/tests/cases/editors/resolve_removal_target_test.gd.uid @@ -0,0 +1 @@ +uid://bbkjp7dksyl5s diff --git a/tests/cases/editors/resolve_unzip_dir_test.gd b/tests/cases/editors/resolve_unzip_dir_test.gd new file mode 100644 index 00000000..665c17dd --- /dev/null +++ b/tests/cases/editors/resolve_unzip_dir_test.gd @@ -0,0 +1,13 @@ +extends GdUnitTestSuite + + +func test_extract_to_same_folder_uses_versions_path_directly() -> void: + assert_str( + RemoteEditorsControl._resolve_unzip_dir("user://__test_versions__", "Godot_v4.5-stable", true) + ).is_equal("user://__test_versions__/") + + +func test_extract_creates_subfolder_named_after_the_release() -> void: + assert_str( + RemoteEditorsControl._resolve_unzip_dir("user://__test_versions__", "Godot_v4.5-stable", false) + ).is_equal("user://__test_versions__/Godot_v4.5-stable/") diff --git a/tests/cases/editors/resolve_unzip_dir_test.gd.uid b/tests/cases/editors/resolve_unzip_dir_test.gd.uid new file mode 100644 index 00000000..37022d6f --- /dev/null +++ b/tests/cases/editors/resolve_unzip_dir_test.gd.uid @@ -0,0 +1 @@ +uid://d2cqe4bdjy68i