Error while disabling my Editor Plugin

Godot Version

4.7.2

Question

Hi everyone,
I’m starting to develop my first plugin, but I’ve encountered my first big obstacle while stress-testing the enable/disable feature of it.The plugin is structured to act as a parent connecting its sub-plugins, which have all a modular structure.
Here is the GDScript file:

@tool
extends EditorPlugin

const PLUGIN_NAME = "plug_and_dev"


var restart_dialog: ConfirmationDialog

func _enable_plugin() -> void:
	E...other sub-plugins
	EditorInterface.set_plugin_enabled(PLUGIN_NAME + "/ui", true)
	
	_show_restart_dialog()


func _disable_plugin() -> void:
	...other sub-plugins...
	EditorInterface.set_plugin_enabled(PLUGIN_NAME + "/ui", false)
	
	_show_restart_dialog()


func _show_restart_dialog() -> void:
	restart_dialog = ConfirmationDialog.new()
	restart_dialog.title = "Restart Required"
	restart_dialog.dialog_text = "Warning: the engine needs to restart, otherwise the plugin won't work properly. \nRestart now?"
	restart_dialog.ok_button_text = "Restart"
	restart_dialog.cancel_button_text = "Later"
	
	if not restart_dialog.confirmed.is_connected(func(): EditorInterface.restart_editor(true)):
		restart_dialog.confirmed.connect(func(): EditorInterface.restart_editor(true))
	if not restart_dialog.canceled.is_connected(func(): restart_dialog.queue_free()):
		restart_dialog.canceled.connect(func(): restart_dialog.queue_free())
	
	var parent_window = get_window()
	if Engine.is_editor_hint():
		parent_window = EditorInterface.get_base_control().get_window()
		parent_window.add_child(restart_dialog)
	
	
	restart_dialog.exclusive = false
	restart_dialog.popup_centered.call_deferred()

The main problem I’m having is with the “input” module.
Following, the GDScript file for the sub-plugin:

@tool
extends EditorPlugin

const BACKUP_FILE_PATH := "plugin_input_backup.cfg"

const INPUT_CONFIG_LIST := [
	{
		"name": "pad_ui_select",
		"kbm": [KEY_ENTER],
		"gamepad": [JOY_BUTTON_A],
		"axis": [],
	},
	{
		"name": "pad_ui_back",
		"kbm": [KEY_ESCAPE],
		"gamepad": [JOY_BUTTON_B],
		"axis": [],
	},
	...other buttons...
]
	
func _enable_plugin() -> void:
	_backup_inputs()
	
	for action in INPUT_CONFIG_LIST:
		var events: Array = []
		
		for input_id in action.kbm:
			if input_id == MOUSE_BUTTON_WHEEL_UP or input_id == MOUSE_BUTTON_WHEEL_DOWN:
				var event_mouse := InputEventMouseButton.new()
				event_mouse.button_index = input_id
				events.append(event_mouse)
			else:
				var event_key := InputEventKey.new()
				event_key.physical_keycode = input_id
				events.append(event_key)
		
		for button_id in action.gamepad:
			var event_joypad := InputEventJoypadButton.new()
			event_joypad.button_index = button_id
			events.append(event_joypad)
		
		if action.axis.size() >= 2:
			var event_axis := InputEventJoypadMotion.new()
			event_axis.axis = action.axis[0]
			event_axis.axis_value = action.axis[1]
			events.append(event_axis)
		
		var action_data := {
			"deadzone": 0.5,
			"events": events
		}
		
		ProjectSettings.set_setting("input/" + action.name, action_data)
	ProjectSettings.save()

func _disable_plugin() -> void:
	for action in INPUT_CONFIG_LIST:
		ProjectSettings.set_setting("input/" + action.name, null)
	
	var properties: Array = ProjectSettings.get_property_list()
	for prop in properties:
		var property_name: String = prop["name"]
		if property_name.begins_with("input/ui_"):
			ProjectSettings.set_setting(property_name, null)
	
	ProjectSettings.save()
	
	_restore_inputs()

func _backup_inputs() -> void:
	var config := ConfigFile.new()
	
	var properties: Array = ProjectSettings.get_property_list()
	
	for prop in properties:
		var property_name: String = prop["name"]
		
		if property_name.begins_with("input/ui_"):
			var original_data = ProjectSettings.get_setting(property_name)
			config.set_value("builtin_inputs", property_name, original_data)
			
			var cleared_data := {
				"deadzone": 0.5,
				"events": []
			}
			
			ProjectSettings.set_setting(property_name, cleared_data)
	config.save(BACKUP_FILE_PATH)

func _restore_inputs() -> void:
	var config := ConfigFile.new()
	
	if config.load(BACKUP_FILE_PATH) == OK:
		if config.has_section("builtin_inputs"):
			var keys = config.get_section_keys("builtin_inputs")
			for property_name in keys:
				var original_data = config.get_value("builtin_inputs", property_name)
				ProjectSettings.set_setting(property_name, original_data)
		
		ProjectSettings.save()
		
		var dir := DirAccess.open("")
		if dir:
			dir.remove(BACKUP_FILE_PATH.get_file())

When I enable the plugin, the engine restarts and all the changes get correctly applied. The “plugin_input_backup.cfg” gets created and written properly.
On the other hand, when disabling the plugin, sometimes it gets disabled correctly, the engine restarts and the backup file gets deleted correctly. Other times, my custom input remiain active, the original ones are left empty and the backup file is still there. The wierd thing is that it mostly happens the second time I make this test (after manually resetting the project file via text editor), while the first time is usually a complete success (but sometimes even that fails).

If it helps, sometimes (with no specific criteria), when I uncheck the plugin to enable it and the confirmation popup appears I see the following error in the console:

“Condition “!p_enabled && !addon_name_to_plugin.has(addon_path)” is true.”

I don’t know if it’s relevant, but I saw it was (apparently) an absolute path problem. I checked and made sure that all paths are relative.

Do these last lines serve any purpose? Can you open an empty directory?

Actually, it represents the current directory, if left empty.

I have this code from one of my projects that disables a plugin. If you notice I have the call_deferred(). You could try deferring it.

	var msg: Window = show_messages(s, "Disable Plugin")
	msg.add_action("Accept", "ACCEPT")
	msg.action_selected.connect(func(action: String):
		match action:
			"OKBUTTON", "CLOSE", "ESCAPE", "CANCEL":
				msg.queue_free()
			"ACCEPT":
				EditorInterface.set_plugin_enabled.call_deferred(plugin_path.get_file(), false)
				msg.queue_free()
			_:
				msg.queue_free()
		)
	pass

IT WORKS! All it needed was the “call_deferred” instruction for every sub-plugin! I can’t hank you enough for this, but still, THANK YOU SO MUCH!!!
Also, I tried specifying the path for the input backup file by adding “res://” and now it consistently deletes the file once the plugin is disabled. For this reason, TECHNICALLY both your answers should be marked as solutions, but I don’t think I can. I’ll mark this one

If you need an easy way to disable/enable plugins in development, without going into Project, you can use the plugin below, I modified it slighlyt from original. Basically it creates a list of all the addons and displays them in the right hand corner as a Plugins menu to enable/disable. Add the folder to your addons and then enable it once. A Plugins menu shows in the top right. From there on you can enable disable any of them quickly with no need to go into the Projects.

Example:

Again, thank you so much!!! I’ll give it a try.