Problem that _handles function is not called immediately after _enable_plugin function is called and drawing cannot be performed

version: v4.3.stable.official [77dcf97d8]
(Translated from Japanese to English using DeepL and Google Translate.)

After the _enable_plugin function of the EditorPlugin derived class is called, the _handles function is not called until the edit node is selected and drawing cannot be performed.

First Things First

This problem is unlikely to occur during normal plugin use, and even if it does occur, it can be easily resolved with a simple operation.
Therefore, this may be a minor problem.
However, this is a problem that is likely to occur during plugin development, so I wanted to post this in the forum to share it as a known problem.

Phenomenon

After creating a plug-in that draws a circle at the mouse cursor position in a 2D workspace using the official sample code, the following problems occur After creating a plug-in, the _handles and _forward_canvas_draw_over_viewport functions are not called immediately after turning the plug-in from “Enabled” to “On” in the “Project Settings” dialog, until another node is selected in the scene dock, and the plug-in cannot draw to the 2D workspace.

How to reproduce

  1. In the “Plug-ins” tab of the “Project Settings” dialog box, check the “Enabled” checkbox OFF, then ON, and close the dialog box.

  2. Place the mouse cursor on the 2D workspace.

  3. A circle is not drawn at the mouse cursor position. (_handles is not called either) ←Problematic part

  4. Select another node in the scene dock. (_handles is called.) ←Current solution

  5. When the mouse cursor is hovered over the 2D workspace, a circle is drawn.

Plug-in script to reproduce the phenomenon

The following is a script for a plug-in that draws a circle at the mouse cursor position in the 2D workspace, including the official sample code.
You can try it out by adding it in the “Plug-ins” tab of the “Project Settings” dialog.

sc_path_tool.gd

@tool
extends EditorPlugin

## ノードが SceneTree に入ったとき (インスタンス化時、シーン変更時、またはスクリプトで add_child を呼び出した後など) に呼び出されます。
func _enter_tree():
	# Initialization of the plugin goes here.
	print("ScPathTool plugin を初期化します")
	
	return

## ノードが SceneTree から出ようとしているときに呼び出されます(たとえば、解放時、シーンの変更時、またはスクリプトで Remove_child を呼び出した後)。
func _exit_tree():
	# Clean-up of the plugin goes here.
	print("ScPathTool plugin を終了します")
	return

## ユーザーがプロジェクト設定ウィンドウの [プラグイン] タブで EditorPlugin を有効にすると、エンジンによって呼び出されます。
func _enable_plugin():
	print("_enable_plugin called")
	return

## ユーザーがプロジェクト設定ウィンドウの [プラグイン] タブで EditorPlugin を無効にすると、エンジンによって呼び出されます。
func _disable_plugin():
	print("_disable_plugin called")
	return

## 2D エディタのビューポートが更新されるときにエンジンによって呼び出されます。
## 描画に [param overlay] コントロールを使用します。
## [method EditorPlugin.update_overlays] を呼び出すことで、ビューポートを手動で更新できます。
func _forward_canvas_draw_over_viewport(overlay: Control) -> void:
	print("_forward_canvas_force_draw_over_viewport called.", overlay)
	# 以下のコードは、公式のサンプルです。
	# https://docs.godotengine.org/ja/4.x/classes/class_editorplugin.html#class-editorplugin-private-method-forward-canvas-draw-over-viewport
	# マウスポインタの位置に円を描きます。
	overlay.draw_circle(overlay.get_local_mouse_position(), 64, Color.WHITE)
	return

## 現在の編集シーンにルートノードがあり、_handlesが実装され、2DビューポートでInputEventが発生したときに呼び出されます。
func _forward_canvas_gui_input(event):
	print("_forward_canvas_gui_input called.", event)
	# 以下のコードは、公式のサンプルです。
	# https://docs.godotengine.org/ja/4.x/classes/class_editorplugin.html#class-editorplugin-private-method-forward-canvas-draw-over-viewport
	if event is InputEventMouseMotion:
		# マウスポインタを移動するとビューポートを再描画します。
		update_overlays()
		return true
	return false

## プラグインが特定のタイプのオブジェクト (リソースまたはノード) を編集する場合は、この関数を実装します。
## true を返すと、エディターが関数 _edit と _make_visible を要求したときに呼び出される関数を取得します。
## _forward_canvas_gui_input メソッドと _forward_3d_gui_input メソッドを宣言している場合、これらも呼び出されます。
func _handles(object: Object) -> bool:
	print("_handles called. object = ", object)
	return true

Scripts created to deal with

By calling the following request_call_editor_plugin_handles_event function in the _enable_plugin function of the plugin’s script, so that the _handles function is called when a node is reselected in the script, TODO: The script for handling this can be found in the “_handles” function.

sc_path_tool.gd (Add it to the _enable_plugin function.)

## ユーザーがプロジェクト設定ウィンドウの [プラグイン] タブで EditorPlugin を有効にすると、エンジンによって呼び出されます。
func _enable_plugin():
	print("_enable_plugin called")
	# 有効になった直後に、 _handles イベント関数の呼び出しを促します。
	SakuraCrowdUtil.request_call_editor_plugin_handles_event(self)
	return

sakuracrowd_util.gd

extends Object
class_name SakuraCrowdUtil

## [method EditorPlugin._handles] イベント関数がシステムから呼び出されるように促します。
## 方法として、選択中のノードの再選択([method SakuraCrowdUtil.reselect_nodes])、
## 選択されていない場合はルートノードの選択([method SakuraCrowdUtil.add_root_node_to_selection])を行います。
## いずれかの方法を行った場合は true, それ以外は false を返します。
static func request_call_editor_plugin_handles_event(editor_plugin: EditorPlugin) -> bool:
	if reselect_nodes(editor_plugin) == false:
		if add_root_node_to_selection(editor_plugin) == false:
			print("SakuraCrowdUtil.request_call_editor_plugin_handles_event: 何も行いませんでした。")
			return false
		else:
			print("SakuraCrowdUtil.request_call_editor_plugin_handles_event: add_root_node_to_selection を行いました。")
	else:
		print("SakuraCrowdUtil.request_call_editor_plugin_handles_event: reselect_nodes を行いました。")
	return true

## 現在選択されているノード群を一度解除してから再び選択します。
## 選択されているノードがない場合はルートノードを再び選択します。
## ルートノードがない場合は何もしません。
## 再選択を行った場合は true, それ以外は false を返します。
static func reselect_nodes(editor_plugin: EditorPlugin) -> bool:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	
	# すべての選択中のノードの選択を解除して、そのノード群を取得します。
	var deselected_nodes: Array[Node] = deselect_nodes(editor_plugin)
	
	if deselected_nodes.size() > 0:
		# 選択解除したノード群を再選択します。
		for node in deselected_nodes:
			editor_interface.get_selection().add_node(node)
		print("SakuraCrowdUtil.reselect_nodes: 選択中のノードを再選択しました。")
		return true
	return false

## 選択中のノード群を解除します。
## 解除したノード群の配列を返します。配列の要素数 size() は 0 かもしれません。
static func deselect_nodes(editor_plugin: EditorPlugin) -> Array[Node]:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	
	# 選択中のノード群を取得します。
	var selected_nodes: Array = editor_interface.get_selection().get_selected_nodes()
	
	# 選択中のノードを1つずつ解除します。
	if selected_nodes.size() > 0:
		for node in selected_nodes:
			editor_interface.get_selection().remove_node(node)
		print("SakuraCrowdUtil.deselect_nodes: 選択中のノードを選択解除しました。")
	
	return selected_nodes

## ルートノードを選択中のノードに追加します。
## 追加した場合は true, それ以外は false を返します。
static func add_root_node_to_selection(editor_plugin: EditorPlugin) -> bool:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	
	# 選択されているノードがない場合、ルートノードを再選択します。
	var scene_tree: SceneTree = editor_plugin.get_tree()
	var root_node: Node = scene_tree.edited_scene_root
	if root_node:
		editor_interface.get_selection().add_node(root_node)
		print("SakuraCrowdUtil.add_root_node_to_selection: ルートノードを選択しました。")
		return true
	else:
		print("SakuraCrowdUtil.add_root_node_to_selection: ノードが選択されていませんでしたし、ルートノードもありませんでした。")
	return false

Reference articles

The following is an article I wrote about the above. It is written in Japanese, but I hope you will find it helpful to refer to the video and screenshot images reproducing the phenomenon.

Article on reproducing the phenomenon
Godot4 プラグインで2Dワークスペースに円を描画するサンプルの実装と確認 | Compota-Soft-Press

Article on the result of adding a script to deal with the problem
Godot4 選択ノードの解除・再選択・ルートノードの選択追加のスクリプト例 | Compota-Soft-Press

I also made a similar post on the Godot Japan forum to share the current workaround.
_enable_plugin 関数が呼ばれた直後、_handles 関数が呼ばれず描画ができない問題 - ナレッジベース - Godot Japan フォーラム
ttps://forum.godot-japan.com/t/topic/60