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