From e5161f397e2d5cbe9c6967b9d466dade2f42d2a8 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 28 Aug 2024 17:43:12 -0700 Subject: [PATCH 01/13] BlockScriptSerialization: Add block library and instantiation This changes BlockScriptSerialization to manage its own list of available block definitions and instantiate them, replacing the existing functionality that was in CategoryFactory and ui/util.gd. The change makes it easier to understand the state of the block catalog, because it is a component of the block script itself instead of spread across different static variables. In addition, this adds a singleton block editor context, which keeps track of the current block code node and its associated block script. The context object provides enough information for different UI components to update automatically when the block script changes. https://phabricator.endlessm.com/T35564 --- addons/block_code/block_code_plugin.gd | 7 +- .../code_generation/blocks_catalog.gd | 36 --- .../block_code/drag_manager/drag_manager.gd | 8 +- .../block_script_serialization.gd | 217 ++++++++++++++++-- .../ui/block_canvas/block_canvas.gd | 28 ++- addons/block_code/ui/block_editor_context.gd | 33 +++ addons/block_code/ui/blocks/block/block.gd | 3 - .../parameter_output/parameter_output.gd | 22 +- addons/block_code/ui/main_panel.gd | 77 ++++--- .../ui/picker/categories/block_category.gd | 13 +- .../categories/block_category_display.gd | 11 +- .../ui/picker/categories/category_factory.gd | 107 +-------- .../create_variable_dialog.gd | 15 +- addons/block_code/ui/picker/picker.gd | 63 +++-- addons/block_code/ui/title_bar/title_bar.gd | 16 +- addons/block_code/ui/util.gd | 43 ---- tests/test_category_factory.gd | 29 ++- tests/test_code_generation.gd | 5 +- 18 files changed, 396 insertions(+), 337 deletions(-) create mode 100644 addons/block_code/ui/block_editor_context.gd diff --git a/addons/block_code/block_code_plugin.gd b/addons/block_code/block_code_plugin.gd index c4370d6b..96b9b7ab 100644 --- a/addons/block_code/block_code_plugin.gd +++ b/addons/block_code/block_code_plugin.gd @@ -1,5 +1,6 @@ @tool extends EditorPlugin + const MainPanelScene := preload("res://addons/block_code/ui/main_panel.tscn") const MainPanel = preload("res://addons/block_code/ui/main_panel.gd") const Types = preload("res://addons/block_code/types/types.gd") @@ -93,16 +94,10 @@ func _exit_tree(): func _ready(): - connect("scene_changed", _on_scene_changed) editor_inspector.connect("edited_object_changed", _on_editor_inspector_edited_object_changed) - _on_scene_changed(EditorInterface.get_edited_scene_root()) _on_editor_inspector_edited_object_changed() -func _on_scene_changed(scene_root: Node): - main_panel.switch_scene(scene_root) - - func _on_editor_inspector_edited_object_changed(): var edited_object = editor_inspector.get_edited_object() #var edited_node = edited_object as Node diff --git a/addons/block_code/code_generation/blocks_catalog.gd b/addons/block_code/code_generation/blocks_catalog.gd index f1fd8e62..6fd38e84 100644 --- a/addons/block_code/code_generation/blocks_catalog.gd +++ b/addons/block_code/code_generation/blocks_catalog.gd @@ -96,40 +96,6 @@ static func _setup_definitions_from_files(): _by_class_name[target][block_definition.name] = block_definition -static func _add_output_definitions(definitions: Array[BlockDefinition]): - # Capture things of format [test] - var _output_regex := RegEx.create_from_string("\\[([^\\]]+)\\]") - - for definition in definitions: - if definition.type != Types.BlockType.ENTRY: - continue - - for reg_match in _output_regex.search_all(definition.display_template): - var parts := reg_match.get_string(1).split(": ") - var param_name := parts[0] - var param_type: Variant.Type = Types.STRING_TO_VARIANT_TYPE[parts[1]] - - var output_def := BlockDefinition.new() - output_def.name = &"%s_%s" % [definition.name, param_name] - output_def.target_node_class = definition.target_node_class - output_def.category = definition.category - output_def.type = Types.BlockType.VALUE - output_def.variant_type = param_type - output_def.display_template = param_name - output_def.code_template = param_name - output_def.scope = definition.code_template - - # Note that these are not added to the _by_class_name dict - # because they only make sense within the entry block scope. - _catalog[output_def.name] = output_def - - -static func _setup_output_definitions(): - var definitions: Array[BlockDefinition] - definitions.assign(_catalog.values()) - _add_output_definitions(definitions) - - static func _add_property_definitions(_class_name: String, property_list: Array[Dictionary], property_settings: Dictionary): for property in property_list: if not property.name in property_settings: @@ -256,7 +222,6 @@ static func setup(): _catalog = {} _setup_definitions_from_files() - _setup_output_definitions() _setup_properties_for_class() _setup_input_block() @@ -330,7 +295,6 @@ static func add_custom_blocks( _catalog[block_definition.name] = block_definition _by_class_name[_class_name][block_definition.name] = block_definition - _add_output_definitions(block_definitions) _add_property_definitions(_class_name, property_list, property_settings) diff --git a/addons/block_code/drag_manager/drag_manager.gd b/addons/block_code/drag_manager/drag_manager.gd index 033f1e8d..74a3fa85 100644 --- a/addons/block_code/drag_manager/drag_manager.gd +++ b/addons/block_code/drag_manager/drag_manager.gd @@ -15,6 +15,8 @@ const Util = preload("res://addons/block_code/ui/util.gd") const Constants = preload("res://addons/block_code/ui/constants.gd") +@onready var _context := BlockEditorContext.get_default() + var _picker: Picker var _block_canvas: BlockCanvas @@ -64,9 +66,9 @@ func drag_block(block: Block, copied_from: Block = null): func copy_block(block: Block) -> Block: - var new_block = Util.instantiate_block(block.definition) - new_block.color = block.color - return new_block + if _context.block_script == null: + return null + return _context.block_script.instantiate_block(block.definition) func copy_picked_block_and_drag(block: Block): diff --git a/addons/block_code/serialization/block_script_serialization.gd b/addons/block_code/serialization/block_script_serialization.gd index 02d61d22..f45844b1 100644 --- a/addons/block_code/serialization/block_script_serialization.gd +++ b/addons/block_code/serialization/block_script_serialization.gd @@ -4,14 +4,23 @@ extends Resource const ASTList = preload("res://addons/block_code/code_generation/ast_list.gd") const BlockAST = preload("res://addons/block_code/code_generation/block_ast.gd") -const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") const BlockCategory = preload("res://addons/block_code/ui/picker/categories/block_category.gd") const BlockDefinition = preload("res://addons/block_code/code_generation/block_definition.gd") const BlockSerialization = preload("res://addons/block_code/serialization/block_serialization.gd") const BlockSerializationTree = preload("res://addons/block_code/serialization/block_serialization_tree.gd") +const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") +const CategoryFactory = preload("res://addons/block_code/ui/picker/categories/category_factory.gd") +const Types = preload("res://addons/block_code/types/types.gd") const ValueBlockSerialization = preload("res://addons/block_code/serialization/value_block_serialization.gd") const VariableDefinition = preload("res://addons/block_code/code_generation/variable_definition.gd") +const SCENE_PER_TYPE = { + Types.BlockType.ENTRY: preload("res://addons/block_code/ui/blocks/entry_block/entry_block.tscn"), + Types.BlockType.STATEMENT: preload("res://addons/block_code/ui/blocks/statement_block/statement_block.tscn"), + Types.BlockType.VALUE: preload("res://addons/block_code/ui/blocks/parameter_block/parameter_block.tscn"), + Types.BlockType.CONTROL: preload("res://addons/block_code/ui/blocks/control_block/control_block.tscn"), +} + @export var script_inherits: String @export var block_serialization_trees: Array[BlockSerializationTree] @export var variables: Array[VariableDefinition]: @@ -19,7 +28,8 @@ const VariableDefinition = preload("res://addons/block_code/code_generation/vari @export var generated_script: String @export var version: int -var _var_block_definitions: Dictionary # String, BlockDefinition +var _available_blocks: Array[BlockDefinition] +var _categories: Array[BlockCategory] func _init( @@ -32,36 +42,108 @@ func _init( version = p_version +func initialize(): + _update_block_definitions() + + func _set_variables(value): variables = value - _refresh_var_block_definitions() + _update_block_definitions() -func _refresh_var_block_definitions(): - _var_block_definitions.clear() - for block_def in BlocksCatalog.get_variable_block_definitions(variables): - _var_block_definitions[block_def.name] = block_def +func instantiate_block(block_definition: BlockDefinition) -> Block: + if block_definition == null: + push_error("Cannot construct block from null block definition.") + return null + var scene := SCENE_PER_TYPE.get(block_definition.type) + if scene == null: + push_error("Cannot instantiate Block from type %s" % block_definition.type) + return null -func _get_block(block_name: StringName) -> BlockDefinition: - var block_def: BlockDefinition = _var_block_definitions.get(block_name) - if block_def == null: - block_def = BlocksCatalog.get_block(block_name) - return block_def + var block_category := _get_category_by_name(block_definition.category) + var block: Block = scene.instantiate() + block.definition = block_definition + block.color = block_category.color if block_category else Color.WHITE + return block -func get_definitions() -> Array[BlockDefinition]: - for class_dict in ProjectSettings.get_global_class_list(): - if class_dict.class == script_inherits: - var script = load(class_dict.path) - if script.has_method("setup_custom_blocks"): - script.setup_custom_blocks() - break - return BlocksCatalog.get_inherited_blocks(script_inherits) +func instantiate_block_by_name(block_name: String) -> Block: + var block_definition := get_block_definition(block_name) + + if block_definition == null: + push_warning("Cannot find a block definition for %s" % block_name) + return null + + return instantiate_block(block_definition) + + +func get_block_definition(block_name: String) -> BlockDefinition: + var split := block_name.split(":", true, 1) + + if len(split) > 1: + return _get_parameter_block_definition(split[0], split[1]) + + var block_definition = _get_base_block_definition(block_name) + + if block_definition == null: + # FIXME: This is a workaround for old-style output block references. + # These were generated ahead of time using a block name that has + # a "_" before the parameter name. Now, these parameter blocks + # are generated on demand for any block name containing a ":". + # Please remove this fallback when it is no longer necessary. + split = block_name.rsplit("_", true, 1) + return _get_parameter_block_definition(split[0], split[1]) + + return block_definition + + +func _get_base_block_definition(block_name: String) -> BlockDefinition: + for block_definition in _available_blocks: + if block_definition.name == block_name: + return block_definition + return null + + +func _get_parameter_block_definition(block_name: String, parameter_name: String) -> BlockDefinition: + var base_block_definition := _get_base_block_definition(block_name) + + if base_block_definition == null: + return null + + var parent_out_parameters = base_block_definition.get_output_parameters() + + if not parent_out_parameters.has(parameter_name): + push_error("The parameter name %s is not an output parameter in %s." % [parameter_name, block_name]) + return null + + var parameter_type: Variant.Type = parent_out_parameters[parameter_name] + + var block_definition := BlockDefinition.new() + block_definition.name = &"%s:%s" % [block_name, parameter_name] + block_definition.target_node_class = base_block_definition.target_node_class + block_definition.category = base_block_definition.category + block_definition.type = Types.BlockType.VALUE + block_definition.variant_type = parameter_type + block_definition.display_template = parameter_name + block_definition.code_template = parameter_name + block_definition.scope = base_block_definition.code_template + + return block_definition + + +func _update_block_definitions(): + _available_blocks.clear() + _available_blocks.append_array(_get_default_block_definitions()) + _available_blocks.append_array(_get_inherited_block_definitions()) + _available_blocks.append_array(_get_variable_block_definitions()) + + var custom_categories: Array[BlockCategory] = _get_custom_categories() + _categories = CategoryFactory.get_all_categories(custom_categories) -func get_categories() -> Array[BlockCategory]: +func _get_custom_categories() -> Array[BlockCategory]: for class_dict in ProjectSettings.get_global_class_list(): if class_dict.class == script_inherits: var script = load(class_dict.path) @@ -71,6 +153,95 @@ func get_categories() -> Array[BlockCategory]: return [] +func get_available_blocks() -> Array[BlockDefinition]: + return _available_blocks + + +func get_available_categories() -> Array[BlockCategory]: + return _categories.filter(func(category): return _available_blocks.any(BlockDefinition.has_category.bind(category.name))) + + +func get_blocks_in_category(category: BlockCategory) -> Array[BlockDefinition]: + return _available_blocks.filter(BlockDefinition.has_category.bind(category.name)) + + +func _get_category_by_name(category_name: String) -> BlockCategory: + return _categories.filter(func(category): return category.name == category_name).front() + + +func load_object_script() -> Object: + for class_dict in ProjectSettings.get_global_class_list(): + if class_dict.class == script_inherits: + return load(class_dict.path) as Object + return null + + +func _get_default_block_definitions() -> Array[BlockDefinition]: + var block: BlockDefinition + var block_list: Array[BlockDefinition] = [] + + # Lifecycle + for block_name in [&"ready", &"process", &"physics_process", &"queue_free"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + # Loops + for block_name in [&"for", &"while", &"break", &"continue", &"await_scene_ready"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + # Logs + block = BlocksCatalog.get_block(&"print") + block_list.append(block) + + # Communication + for block_name in [&"define_method", &"call_method_group", &"call_method_node"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + for block_name in [&"add_to_group", &"add_node_to_group", &"remove_from_group", &"remove_node_from_group", &"is_in_group", &"is_node_in_group"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + # Variables + block = BlocksCatalog.get_block(&"vector2") + block_list.append(block) + + # Math + for block_name in [&"add", &"subtract", &"multiply", &"divide", &"pow", &"randf_range", &"randi_range", &"sin", &"cos", &"tan"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + # Logic + for block_name in [&"if", &"else_if", &"else", &"compare", &"and", &"or", &"not"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + # Input + block = BlocksCatalog.get_block(&"is_input_actioned") + block_list.append(block) + + # Sounds + for block_name in [&"load_sound", &"play_sound", &"pause_continue_sound", &"stop_sound"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + # Graphics + for block_name in [&"viewport_width", &"viewport_height", &"viewport_center"]: + block = BlocksCatalog.get_block(block_name) + block_list.append(block) + + return block_list + + +func _get_inherited_block_definitions() -> Array[BlockDefinition]: + return BlocksCatalog.get_inherited_blocks(script_inherits) + + +func _get_variable_block_definitions() -> Array[BlockDefinition]: + return BlocksCatalog.get_variable_block_definitions(variables) + + func generate_ast_list() -> ASTList: var ast_list := ASTList.new() for tree in block_serialization_trees: @@ -87,7 +258,7 @@ func _tree_to_ast(tree: BlockSerializationTree) -> BlockAST: func _block_to_ast_node(node: BlockSerialization) -> BlockAST.ASTNode: var ast_node := BlockAST.ASTNode.new() - ast_node.data = _get_block(node.name) + ast_node.data = get_block_definition(node.name) for arg_name in node.arguments: var argument = node.arguments[arg_name] @@ -105,7 +276,7 @@ func _block_to_ast_node(node: BlockSerialization) -> BlockAST.ASTNode: func _value_to_ast_value(value_node: ValueBlockSerialization) -> BlockAST.ASTValueNode: var ast_value_node := BlockAST.ASTValueNode.new() - ast_value_node.data = _get_block(value_node.name) + ast_value_node.data = get_block_definition(value_node.name) for arg_name in value_node.arguments: var argument = value_node.arguments[arg_name] diff --git a/addons/block_code/ui/block_canvas/block_canvas.gd b/addons/block_code/ui/block_canvas/block_canvas.gd index c3095170..34b1c3e8 100644 --- a/addons/block_code/ui/block_canvas/block_canvas.gd +++ b/addons/block_code/ui/block_canvas/block_canvas.gd @@ -15,6 +15,8 @@ const DEFAULT_WINDOW_MARGIN: Vector2 = Vector2(25, 25) const SNAP_GRID: Vector2 = Vector2(25, 25) const ZOOM_FACTOR: float = 1.1 +@onready var _context := BlockEditorContext.get_default() + @onready var _window: Control = %Window @onready var _empty_box: BoxContainer = %EmptyBox @@ -52,6 +54,8 @@ signal replace_block_code func _ready(): + _context.changed.connect(_on_context_changed) + if not _open_scene_button.icon and not Util.node_is_part_of_edited_scene(self): _open_scene_button.icon = _open_scene_icon @@ -87,12 +91,12 @@ func set_child(n: Node): set_child(c) -func block_script_selected(block_script: BlockScriptSerialization): +func _on_context_changed(): clear_canvas() var edited_node = EditorInterface.get_inspector().get_edited_object() as Node - if block_script != _current_block_script: + if _context.block_script != _current_block_script: _window.position = Vector2(0, 0) zoom = 1 @@ -106,12 +110,12 @@ func block_script_selected(block_script: BlockScriptSerialization): _open_scene_button.disabled = true _replace_block_code_button.disabled = true - if block_script != null: - _load_block_script(block_script) + if _context.block_script != null: + _load_block_script(_context.block_script) _window.visible = true _zoom_button.visible = true - if block_script != _current_block_script: + if _context.block_script != _current_block_script: reset_window_position() elif edited_node == null: _empty_box.visible = true @@ -130,7 +134,7 @@ func block_script_selected(block_script: BlockScriptSerialization): _selected_node_label.text = _selected_node_label_format.format({"node": edited_node.name}) _add_block_code_button.disabled = false - _current_block_script = block_script + _current_block_script = _context.block_script func _load_block_script(block_script: BlockScriptSerialization): @@ -146,8 +150,8 @@ func reload_ui_from_ast_list(): func ui_tree_from_ast_node(ast_node: BlockAST.ASTNode) -> Block: - var block: Block = Util.instantiate_block(ast_node.data) - block.color = Util.get_category_color(ast_node.data.category) + var block: Block = _context.block_script.instantiate_block(ast_node.data) + # Args for arg_name in ast_node.arguments: var argument = ast_node.arguments[arg_name] @@ -177,8 +181,8 @@ func ui_tree_from_ast_node(ast_node: BlockAST.ASTNode) -> Block: func ui_tree_from_ast_value_node(ast_value_node: BlockAST.ASTValueNode) -> Block: - var block: Block = Util.instantiate_block(ast_value_node.data) - block.color = Util.get_category_color(ast_value_node.data.category) + var block: Block = _context.block_script.instantiate_block(ast_value_node.data) + # Args for arg_name in ast_value_node.arguments: var argument = ast_value_node.arguments[arg_name] @@ -259,7 +263,7 @@ func build_value_ast(block: ParameterBlock) -> BlockAST.ASTValueNode: func rebuild_block_serialization_trees(): - _current_block_script.update_from_ast_list(_current_ast_list) + _context.block_script.update_from_ast_list(_current_ast_list) func find_snaps(node: Node) -> Array[SnapPoint]: @@ -383,7 +387,7 @@ func set_mouse_override(override: bool): func generate_script_from_current_window() -> String: - return ScriptGenerator.generate_script(_current_ast_list, _current_block_script) + return ScriptGenerator.generate_script(_current_ast_list, _context.block_script) func _on_zoom_button_pressed(): diff --git a/addons/block_code/ui/block_editor_context.gd b/addons/block_code/ui/block_editor_context.gd new file mode 100644 index 00000000..6c36bba1 --- /dev/null +++ b/addons/block_code/ui/block_editor_context.gd @@ -0,0 +1,33 @@ +class_name BlockEditorContext +extends Object + +signal changed + +static var _instance: BlockEditorContext + +var block_code_node: BlockCode: + set(value): + block_code_node = value + changed.emit() + +var block_script: BlockScriptSerialization: + get: + if block_code_node == null: + return null + return block_code_node.block_script + +var parent_node: Node: + get: + if block_code_node == null: + return null + return block_code_node.get_parent() + + +func force_update() -> void: + changed.emit() + + +static func get_default() -> BlockEditorContext: + if _instance == null: + _instance = BlockEditorContext.new() + return _instance diff --git a/addons/block_code/ui/blocks/block/block.gd b/addons/block_code/ui/blocks/block/block.gd index 741d31c4..499927ca 100644 --- a/addons/block_code/ui/blocks/block/block.gd +++ b/addons/block_code/ui/blocks/block/block.gd @@ -10,9 +10,6 @@ signal modified ## Color of block (optionally used to draw block color) @export var color: Color = Color(1., 1., 1.) -## Category to add the block to -@export var category: String - # FIXME Note: This used to be a NodePath. There is a bug in Godot 4.2 that causes the # reference to not be set properly when the node is duplicated. Since we don't # use the Node duplicate function anymore, this is okay. diff --git a/addons/block_code/ui/blocks/utilities/parameter_output/parameter_output.gd b/addons/block_code/ui/blocks/utilities/parameter_output/parameter_output.gd index 0fb715cc..579d3102 100644 --- a/addons/block_code/ui/blocks/utilities/parameter_output/parameter_output.gd +++ b/addons/block_code/ui/blocks/utilities/parameter_output/parameter_output.gd @@ -5,10 +5,15 @@ extends MarginContainer const Types = preload("res://addons/block_code/types/types.gd") var block: Block -var output_block: Block +var parameter_name: String +var _block_name: String: + get: + return block.definition.name if block else "" @export var block_params: Dictionary +@onready var _context := BlockEditorContext.get_default() + @onready var _snap_point := %SnapPoint @@ -22,10 +27,17 @@ func _update_parameter_block(): if _snap_point.has_snapped_block(): return - var parameter_block = preload("res://addons/block_code/ui/blocks/parameter_block/parameter_block.tscn").instantiate() - for key in block_params: - parameter_block[key] = block_params[key] - parameter_block.spawned_by = self + if _context.block_script == null: + return + + var block_name = &"%s:%s" % [_block_name, parameter_name] + var parameter_block: ParameterBlock = _context.block_script.instantiate_block_by_name(block_name) + + if parameter_block == null: + # FIXME: This sometimes occurs when a script is loaded but it is unclear why + #push_error("Unable to create output block %s." % block_name) + return + _snap_point.add_child.call_deferred(parameter_block) diff --git a/addons/block_code/ui/main_panel.gd b/addons/block_code/ui/main_panel.gd index 64ea1f90..cb4dabd5 100644 --- a/addons/block_code/ui/main_panel.gd +++ b/addons/block_code/ui/main_panel.gd @@ -5,11 +5,14 @@ signal script_window_requested(script: String) const BlockCanvas = preload("res://addons/block_code/ui/block_canvas/block_canvas.gd") const BlockCodePlugin = preload("res://addons/block_code/block_code_plugin.gd") +const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") const DragManager = preload("res://addons/block_code/drag_manager/drag_manager.gd") const Picker = preload("res://addons/block_code/ui/picker/picker.gd") const TitleBar = preload("res://addons/block_code/ui/title_bar/title_bar.gd") const VariableDefinition = preload("res://addons/block_code/code_generation/variable_definition.gd") +@onready var _context := BlockEditorContext.get_default() + @onready var _picker: Picker = %Picker @onready var _block_canvas: BlockCanvas = %BlockCanvas @onready var _drag_manager: DragManager = %DragManager @@ -25,7 +28,6 @@ const VariableDefinition = preload("res://addons/block_code/code_generation/vari const Constants = preload("res://addons/block_code/ui/constants.gd") -var _current_block_code_node: BlockCode var _block_code_nodes: Array var _collapsed: bool = false @@ -39,6 +41,8 @@ var undo_redo: EditorUndoRedoManager: func _ready(): + _context.changed.connect(_on_context_changed) + _picker.block_picked.connect(_drag_manager.copy_picked_block_and_drag) _picker.variable_created.connect(_create_variable) _block_canvas.reconnect_block.connect(_drag_manager.connect_block_canvas_signals) @@ -52,13 +56,7 @@ func _ready(): func _on_undo_redo_version_changed(): - if _current_block_code_node == null: - return - - var block_script: BlockScriptSerialization = _current_block_code_node.block_script - _picker.block_script_selected(block_script) - _title_bar.block_script_selected(block_script) - _block_canvas.block_script_selected(block_script) + _context.force_update() func _on_show_script_button_pressed(): @@ -73,15 +71,14 @@ func _on_delete_node_button_pressed(): if not scene_root: return - if not _current_block_code_node: + if not _context.block_code_node: return var dialog = ConfirmationDialog.new() var text_format: String = 'Delete block code ("{node}") for "{parent}"?' - dialog.dialog_text = text_format.format({"node": _current_block_code_node.name, "parent": _current_block_code_node.get_parent().name}) + dialog.dialog_text = text_format.format({"node": _context.block_code_node.name, "parent": _context.parent_node.name}) EditorInterface.popup_dialog_centered(dialog) - dialog.connect("confirmed", _on_delete_dialog_confirmed.bind(_current_block_code_node)) - pass # Replace with function body. + dialog.connect("confirmed", _on_delete_dialog_confirmed.bind(_context.block_code_node)) func _on_delete_dialog_confirmed(block_code_node: BlockCode): @@ -90,7 +87,7 @@ func _on_delete_dialog_confirmed(block_code_node: BlockCode): if not parent_node: return - undo_redo.create_action("Delete %s's block code script" % _current_block_code_node.get_parent().name, UndoRedo.MERGE_DISABLE, parent_node) + undo_redo.create_action("Delete %s's block code script" % parent_node.name, UndoRedo.MERGE_DISABLE, parent_node) undo_redo.add_do_property(block_code_node, "owner", null) undo_redo.add_do_method(parent_node, "remove_child", block_code_node) undo_redo.add_undo_method(parent_node, "add_child", block_code_node) @@ -100,53 +97,59 @@ func _on_delete_dialog_confirmed(block_code_node: BlockCode): func _try_migration(): - var version: int = _current_block_code_node.block_script.version + var version: int = _context.block_script.version if version == Constants.CURRENT_DATA_VERSION: # No migration needed. return push_warning("Migration not implemented from %d to %d" % [version, Constants.CURRENT_DATA_VERSION]) -func switch_scene(scene_root: Node): - _title_bar.scene_selected(scene_root) +func switch_block_code_node(block_code_node: BlockCode): + BlocksCatalog.setup() + + var block_script := block_code_node.block_script if block_code_node != null else null + var object_script := block_script.load_object_script() if block_script != null else null + if object_script and object_script.has_method("setup_custom_blocks"): + object_script.setup_custom_blocks() -func switch_block_code_node(block_code_node: BlockCode): - var block_script: BlockScriptSerialization = block_code_node.block_script if block_code_node else null - _current_block_code_node = block_code_node - _delete_node_button.disabled = _current_block_code_node == null - if _current_block_code_node != null: + if block_script: + block_script.initialize() + + _context.block_code_node = block_code_node + + +func _on_context_changed(): + _delete_node_button.disabled = _context.block_code_node == null + if _context.block_code_node != null: _try_migration() - _picker.block_script_selected(block_script) - _title_bar.block_script_selected(block_script) - _block_canvas.block_script_selected(block_script) func save_script(): - if _current_block_code_node == null: + if _context.block_code_node == null: print("No script loaded to save.") return var scene_node = EditorInterface.get_edited_scene_root() - if not BlockCodePlugin.is_block_code_editable(_current_block_code_node): - print("Block code for {node} is not editable.".format({"node": _current_block_code_node})) + if not BlockCodePlugin.is_block_code_editable(_context.block_code_node): + print("Block code for {node} is not editable.".format({"node": _context.block_code_node})) return - var block_script: BlockScriptSerialization = _current_block_code_node.block_script + var block_script: BlockScriptSerialization = _context.block_script var resource_path_split = block_script.resource_path.split("::", true, 1) var resource_scene = resource_path_split[0] - undo_redo.create_action("Modify %s's block code script" % _current_block_code_node.get_parent().name, UndoRedo.MERGE_DISABLE, _current_block_code_node) + undo_redo.create_action("Modify %s's block code script" % _context.parent_node.name, UndoRedo.MERGE_DISABLE, _context.block_code_node) if resource_scene and resource_scene != scene_node.scene_file_path: # This resource is from another scene. Since the user is changing it # here, we'll make a copy for this scene rather than changing it in the # other scene file. - undo_redo.add_undo_property(_current_block_code_node, "block_script", _current_block_code_node.block_script) + undo_redo.add_undo_property(_context.block_code_node, "block_script", _context.block_script) block_script = block_script.duplicate(true) - undo_redo.add_do_property(_current_block_code_node, "block_script", block_script) + undo_redo.add_do_property(_context.block_code_node, "block_script", block_script) undo_redo.add_undo_property(block_script, "block_serialization_trees", block_script.block_serialization_trees) _block_canvas.rebuild_ast_list() @@ -264,19 +267,19 @@ func _set_selection(nodes: Array[Node]): func _create_variable(variable: VariableDefinition): - if _current_block_code_node == null: + if _context.block_code_node == null: print("No script loaded to add variable to.") return - var block_script: BlockScriptSerialization = _current_block_code_node.block_script + var block_script: BlockScriptSerialization = _context.block_script - undo_redo.create_action("Create variable %s in %s's block code script" % [variable.var_name, _current_block_code_node.get_parent().name]) - undo_redo.add_undo_property(_current_block_code_node.block_script, "variables", _current_block_code_node.block_script.variables) + undo_redo.create_action("Create variable %s in %s's block code script" % [variable.var_name, _context.parent_node.name]) + undo_redo.add_undo_property(_context.block_script, "variables", _context.block_script.variables) var new_variables = block_script.variables.duplicate() new_variables.append(variable) - undo_redo.add_do_property(_current_block_code_node.block_script, "variables", new_variables) + undo_redo.add_do_property(_context.block_script, "variables", new_variables) undo_redo.commit_action() - _picker.reload_variables(new_variables) + _picker.reload_blocks() diff --git a/addons/block_code/ui/picker/categories/block_category.gd b/addons/block_code/ui/picker/categories/block_category.gd index e076865d..1169c35a 100644 --- a/addons/block_code/ui/picker/categories/block_category.gd +++ b/addons/block_code/ui/picker/categories/block_category.gd @@ -1,15 +1,18 @@ extends RefCounted -const BlockDefinition = preload("res://addons/block_code/code_generation/block_definition.gd") - var name: String -var block_list: Array[BlockDefinition] var color: Color var order: int -func _init(p_name: String = "", p_color: Color = Color.WHITE, p_order: int = 0, p_block_list: Array[BlockDefinition] = []): +func _init(p_name: String = "", p_color: Color = Color.WHITE, p_order: int = 0): name = p_name - block_list = p_block_list color = p_color order = p_order + + +## Compare block categories for sorting. Compare by order then name. +static func sort_by_order(a, b) -> bool: + if a.order != b.order: + return a.order < b.order + return a.name.naturalcasecmp_to(b.name) < 0 diff --git a/addons/block_code/ui/picker/categories/block_category_display.gd b/addons/block_code/ui/picker/categories/block_category_display.gd index 5145836a..73fd1e63 100644 --- a/addons/block_code/ui/picker/categories/block_category_display.gd +++ b/addons/block_code/ui/picker/categories/block_category_display.gd @@ -8,15 +8,20 @@ const Util = preload("res://addons/block_code/ui/util.gd") var category: BlockCategory +@onready var _context := BlockEditorContext.get_default() + @onready var _label := %Label @onready var _blocks := %Blocks func _ready(): - _label.text = category.name + _label.text = category.name if category != null else "" + + if _context.block_script == null: + return - for block_definition in category.block_list: - var block: Block = Util.instantiate_block(block_definition) + for block_definition in _context.block_script.get_blocks_in_category(category): + var block: Block = _context.block_script.instantiate_block(block_definition) block.color = category.color block.can_delete = false diff --git a/addons/block_code/ui/picker/categories/category_factory.gd b/addons/block_code/ui/picker/categories/category_factory.gd index 02eda12b..2a508d33 100644 --- a/addons/block_code/ui/picker/categories/category_factory.gd +++ b/addons/block_code/ui/picker/categories/category_factory.gd @@ -2,105 +2,20 @@ class_name CategoryFactory extends Object const BlockCategory = preload("res://addons/block_code/ui/picker/categories/block_category.gd") -const BlockDefinition = preload("res://addons/block_code/code_generation/block_definition.gd") -const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") -const Types = preload("res://addons/block_code/types/types.gd") -const Util = preload("res://addons/block_code/ui/util.gd") const Constants = preload("res://addons/block_code/ui/constants.gd") -## Compare block categories for sorting. Compare by order then name. -static func _category_cmp(a: BlockCategory, b: BlockCategory) -> bool: - if a.order != b.order: - return a.order < b.order - return a.name.naturalcasecmp_to(b.name) < 0 +## Returns a list of BlockCategory instances for all block categories. +static func get_all_categories(custom_categories: Array[BlockCategory] = []) -> Array[BlockCategory]: + var result: Array[BlockCategory] + for category_name in Constants.BUILTIN_CATEGORIES_PROPS: + var props: Dictionary = Constants.BUILTIN_CATEGORIES_PROPS.get(category_name, {}) + var color: Color = props.get("color", Color.SLATE_GRAY) + var order: int = props.get("order", 0) + result.append(BlockCategory.new(category_name, color, order)) -static func get_categories(blocks: Array[BlockDefinition], extra_categories: Array[BlockCategory] = []) -> Array[BlockCategory]: - var cat_map: Dictionary = {} - var extra_cat_map: Dictionary = {} + # TODO: Should we deduplicate custom_categories here? + result.append_array(custom_categories) - for cat in extra_categories: - extra_cat_map[cat.name] = cat - - for block in blocks: - var cat: BlockCategory = cat_map.get(block.category) - if cat == null: - cat = extra_cat_map.get(block.category) - if cat == null: - var props: Dictionary = Constants.BUILTIN_CATEGORIES_PROPS.get(block.category, {}) - var color: Color = props.get("color", Color.SLATE_GRAY) - var order: int = props.get("order", 0) - cat = BlockCategory.new(block.category, color, order) - cat_map[block.category] = cat - cat.block_list.append(block) - - # Dictionary.values() returns an untyped Array and there's no way to - # convert an array type besides Array.assign(). - var cats: Array[BlockCategory] = [] - cats.assign(cat_map.values()) - # Accessing a static Callable from a static function fails in 4.2.1. - # Use the fully qualified name. - # https://github.com/godotengine/godot/issues/86032 - cats.sort_custom(CategoryFactory._category_cmp) - return cats - - -static func get_general_blocks() -> Array[BlockDefinition]: - var block: BlockDefinition - var block_list: Array[BlockDefinition] = [] - - BlocksCatalog.setup() - - # Lifecycle - for block_name in [&"ready", &"process", &"physics_process", &"queue_free"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - # Loops - for block_name in [&"for", &"while", &"break", &"continue", &"await_scene_ready"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - # Logs - block = BlocksCatalog.get_block(&"print") - block_list.append(block) - - # Communication - for block_name in [&"define_method", &"call_method_group", &"call_method_node"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - for block_name in [&"add_to_group", &"add_node_to_group", &"remove_from_group", &"remove_node_from_group", &"is_in_group", &"is_node_in_group"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - # Variables - block = BlocksCatalog.get_block(&"vector2") - block_list.append(block) - - # Math - for block_name in [&"add", &"subtract", &"multiply", &"divide", &"pow", &"randf_range", &"randi_range", &"sin", &"cos", &"tan"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - # Logic - for block_name in [&"if", &"else_if", &"else", &"compare", &"and", &"or", &"not"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - # Input - block = BlocksCatalog.get_block(&"is_input_actioned") - block_list.append(block) - - # Sounds - for block_name in [&"load_sound", &"play_sound", &"pause_continue_sound", &"stop_sound"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - # Graphics - for block_name in [&"viewport_width", &"viewport_height", &"viewport_center"]: - block = BlocksCatalog.get_block(block_name) - block_list.append(block) - - return block_list + return result diff --git a/addons/block_code/ui/picker/categories/variable_category/create_variable_dialog.gd b/addons/block_code/ui/picker/categories/variable_category/create_variable_dialog.gd index cc818d43..8383bbb5 100644 --- a/addons/block_code/ui/picker/categories/variable_category/create_variable_dialog.gd +++ b/addons/block_code/ui/picker/categories/variable_category/create_variable_dialog.gd @@ -5,6 +5,8 @@ const BlockCodePlugin = preload("res://addons/block_code/block_code_plugin.gd") signal create_variable(var_name: String, var_type: String) +@onready var _context := BlockEditorContext.get_default() + @onready var _variable_input := %VariableInput @onready var _type_option := %TypeOption @onready var _messages := %Messages @@ -60,14 +62,11 @@ func check_errors(new_var_name: String) -> bool: errors.append("Variable name cannot contain special characters") var duplicate_variable_name := false - var current_block_code = BlockCodePlugin.main_panel._current_block_code_node - if current_block_code: - var current_block_script = current_block_code.block_script - if current_block_script: - for variable in current_block_script.variables: - if variable.var_name == new_var_name: - duplicate_variable_name = true - break + if _context.block_script: + for variable in _context.block_script.variables: + if variable.var_name == new_var_name: + duplicate_variable_name = true + break if duplicate_variable_name: errors.append("Variable already exists") diff --git a/addons/block_code/ui/picker/picker.gd b/addons/block_code/ui/picker/picker.gd index d2fbd4c2..49b90d81 100644 --- a/addons/block_code/ui/picker/picker.gd +++ b/addons/block_code/ui/picker/picker.gd @@ -6,7 +6,6 @@ const BlockCategory = preload("res://addons/block_code/ui/picker/categories/bloc const BlockCategoryButtonScene = preload("res://addons/block_code/ui/picker/categories/block_category_button.tscn") const BlockCategoryButton = preload("res://addons/block_code/ui/picker/categories/block_category_button.gd") const BlockCategoryDisplay = preload("res://addons/block_code/ui/picker/categories/block_category_display.gd") -const CategoryFactory = preload("res://addons/block_code/ui/picker/categories/category_factory.gd") const Util = preload("res://addons/block_code/ui/util.gd") const VariableCategoryDisplay = preload("res://addons/block_code/ui/picker/categories/variable_category/variable_category_display.gd") const VariableDefinition = preload("res://addons/block_code/code_generation/variable_definition.gd") @@ -14,6 +13,8 @@ const VariableDefinition = preload("res://addons/block_code/code_generation/vari signal block_picked(block: Block) signal variable_created(variable: VariableDefinition) +@onready var _context := BlockEditorContext.get_default() + @onready var _block_list := %BlockList @onready var _block_scroll := %BlockScroll @onready var _category_list := %CategoryList @@ -23,35 +24,31 @@ var scroll_tween: Tween var _variable_category_display: VariableCategoryDisplay = null -func block_script_selected(block_script: BlockScriptSerialization): - if not block_script: - reset_picker() - return +func _ready() -> void: + _context.changed.connect(_on_context_changed) - var blocks_to_add: Array[BlockDefinition] = block_script.get_definitions() - var categories_to_add: Array[BlockCategory] = block_script.get_categories() - init_picker(blocks_to_add, categories_to_add) - reload_variables(block_script.variables) +func _on_context_changed(): + _block_scroll.scroll_vertical = 0 + _update_block_components() -func reset_picker(): - for c in _category_list.get_children(): - c.queue_free() +func reload_blocks(): + _update_block_components() - for c in _block_list.get_children(): - c.queue_free() +func _update_block_components(): + # FIXME: Instead, we should reuse existing CategoryList and BlockList components. + _reset_picker() -func init_picker(extra_blocks: Array[BlockDefinition] = [], extra_categories: Array[BlockCategory] = []): - reset_picker() + if not _context.block_script: + return - var blocks := CategoryFactory.get_general_blocks() + extra_blocks - var block_categories := CategoryFactory.get_categories(blocks, extra_categories) + var block_categories := _context.block_script.get_available_categories() - for _category in block_categories: - var category: BlockCategory = _category as BlockCategory + block_categories.sort_custom(BlockCategory.sort_by_order) + for category in block_categories: var block_category_button: BlockCategoryButton = BlockCategoryButtonScene.instantiate() block_category_button.category = category block_category_button.selected.connect(_category_selected) @@ -71,7 +68,15 @@ func init_picker(extra_blocks: Array[BlockDefinition] = [], extra_categories: Ar _block_list.add_child(block_category_display) - _block_scroll.scroll_vertical = 0 + +func _reset_picker(): + for node in _category_list.get_children(): + _category_list.remove_child(node) + node.queue_free() + + for node in _block_list.get_children(): + _block_list.remove_child(node) + node.queue_free() func scroll_to(y: float): @@ -90,19 +95,3 @@ func _category_selected(category: BlockCategory): func set_collapsed(collapsed: bool): _widget_container.visible = not collapsed - - -func reload_variables(variables: Array[VariableDefinition]): - if _variable_category_display: - for c in _variable_category_display.variable_blocks.get_children(): - c.queue_free() - - var i := 1 - for block in Util.instantiate_variable_blocks(variables): - _variable_category_display.variable_blocks.add_child(block) - block.drag_started.connect(func(block: Block): block_picked.emit(block)) - if i % 2 == 0: - var spacer := Control.new() - spacer.custom_minimum_size.y = 12 - _variable_category_display.variable_blocks.add_child(spacer) - i += 1 diff --git a/addons/block_code/ui/title_bar/title_bar.gd b/addons/block_code/ui/title_bar/title_bar.gd index 710c4344..f2dfc45c 100644 --- a/addons/block_code/ui/title_bar/title_bar.gd +++ b/addons/block_code/ui/title_bar/title_bar.gd @@ -5,6 +5,8 @@ const BlockCodePlugin = preload("res://addons/block_code/block_code_plugin.gd") signal node_name_changed(node_name: String) +@onready var _context := BlockEditorContext.get_default() + @onready var _block_code_icon = load("res://addons/block_code/block_code_node/block_code_node.svg") as Texture2D @onready var _editor_inspector: EditorInspector = EditorInterface.get_inspector() @onready var _editor_selection: EditorSelection = EditorInterface.get_selection() @@ -12,17 +14,11 @@ signal node_name_changed(node_name: String) func _ready(): + _context.changed.connect(_on_context_changed) _node_option_button.connect("item_selected", _on_node_option_button_item_selected) -func scene_selected(scene_root: Node): - _update_node_option_button_items() - var current_block_code = _editor_inspector.get_edited_object() as BlockCode - if not current_block_code: - block_script_selected(null) - - -func block_script_selected(block_script: BlockScriptSerialization): +func _on_context_changed(): # TODO: We should listen for property changes in all BlockCode nodes and # their parents. As a workaround for the UI displaying stale data, # we'll crudely update the list of BlockCode nodes whenever the @@ -30,7 +26,7 @@ func block_script_selected(block_script: BlockScriptSerialization): _update_node_option_button_items() - var select_index = _get_block_script_index(block_script) + var select_index = _get_block_script_index(_context.block_script) if _node_option_button.selected != select_index: _node_option_button.select(select_index) @@ -53,6 +49,8 @@ func _update_node_option_button_items(): _node_option_button.set_item_icon(node_item_index, _block_code_icon) _node_option_button.set_item_metadata(node_item_index, block_code) + _node_option_button.disabled = _node_option_button.item_count == 0 + func _get_block_script_index(block_script: BlockScriptSerialization) -> int: for index in range(_node_option_button.item_count): diff --git a/addons/block_code/ui/util.gd b/addons/block_code/ui/util.gd index 076d1f84..46918d2f 100644 --- a/addons/block_code/ui/util.gd +++ b/addons/block_code/ui/util.gd @@ -1,48 +1,5 @@ extends Object -const BlockDefinition = preload("res://addons/block_code/code_generation/block_definition.gd") -const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") -const Types = preload("res://addons/block_code/types/types.gd") -const Constants = preload("res://addons/block_code/ui/constants.gd") -const VariableDefinition = preload("res://addons/block_code/code_generation/variable_definition.gd") - -const SCENE_PER_TYPE = { - Types.BlockType.ENTRY: preload("res://addons/block_code/ui/blocks/entry_block/entry_block.tscn"), - Types.BlockType.STATEMENT: preload("res://addons/block_code/ui/blocks/statement_block/statement_block.tscn"), - Types.BlockType.VALUE: preload("res://addons/block_code/ui/blocks/parameter_block/parameter_block.tscn"), - Types.BlockType.CONTROL: preload("res://addons/block_code/ui/blocks/control_block/control_block.tscn"), -} - - -static func get_category_color(category: String) -> Color: - var category_props: Dictionary = Constants.BUILTIN_CATEGORIES_PROPS.get(category, {}) - return category_props.get("color", Color.SLATE_GRAY) - - -static func instantiate_block(block_definition: BlockDefinition) -> Block: - if block_definition == null: - push_error("Cannot construct block from null block definition.") - return null - - var scene = SCENE_PER_TYPE.get(block_definition.type) - if scene == null: - push_error("Cannot instantiate Block from type %s" % block_definition.type) - return null - - var block = scene.instantiate() - block.definition = block_definition - return block - - -static func instantiate_variable_blocks(variables: Array[VariableDefinition]) -> Array[Block]: - var blocks: Array[Block] = [] - for block_definition in BlocksCatalog.get_variable_block_definitions(variables): - var block = instantiate_block(block_definition) - block.color = get_category_color(block_definition.category) - blocks.append(block) - - return blocks - ## Polyfill of Node.is_part_of_edited_scene(), available to GDScript in Godot 4.3+. static func node_is_part_of_edited_scene(node: Node) -> bool: diff --git a/tests/test_category_factory.gd b/tests/test_category_factory.gd index 99dbfd11..4273b7f6 100644 --- a/tests/test_category_factory.gd +++ b/tests/test_category_factory.gd @@ -1,27 +1,36 @@ extends GutTest -## Tests for CategoryFactory +## Tests for BlockFactory const BlockDefinition = preload("res://addons/block_code/code_generation/block_definition.gd") const BlockCategory = preload("res://addons/block_code/ui/picker/categories/block_category.gd") const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") +var block_script: BlockScriptSerialization + func get_category_names(categories: Array[BlockCategory]) -> Array[String]: - var names: Array[String] = [] - for category in categories: - names.append(category.name) - return names + var categories_sorted: Array[BlockCategory] + categories_sorted.assign(categories) + categories_sorted.sort_custom(BlockCategory.sort_by_order) + var result: Array[String] + result.assign(categories_sorted.map(func(category): return category.name)) + return result func get_class_category_names(_class_name: String) -> Array[String]: var blocks: Array[BlockDefinition] = BlocksCatalog.get_inherited_blocks(_class_name) - var names: Array[String] = get_category_names(CategoryFactory.get_categories(blocks)) - return names + var categories: Array[BlockCategory] = block_script._categories.filter(func(category): return blocks.any(func(block): return block.category == category.name)) + return get_category_names(categories) + + +func before_each(): + block_script = BlockScriptSerialization.new() + block_script.initialize() func test_general_category_names(): - var blocks: Array[BlockDefinition] = CategoryFactory.get_general_blocks() - var names: Array[String] = get_category_names(CategoryFactory.get_categories(blocks)) + var blocks: Array[BlockDefinition] = block_script.get_available_blocks() + var names: Array[String] = get_category_names(block_script.get_available_categories()) assert_eq( names, [ @@ -55,7 +64,7 @@ func test_inherited_category_names(params = use_parameters(class_category_names) func test_unique_block_names(): - var blocks: Array[BlockDefinition] = CategoryFactory.get_general_blocks() + var blocks: Array[BlockDefinition] = block_script.get_available_blocks() var block_names: Dictionary for block in blocks: assert_does_not_have(block_names, block.name, "Block name %s is duplicated" % block.name) diff --git a/tests/test_code_generation.gd b/tests/test_code_generation.gd index 7e61602e..9e960480 100644 --- a/tests/test_code_generation.gd +++ b/tests/test_code_generation.gd @@ -9,6 +9,7 @@ const BlockDefinition = preload("res://addons/block_code/code_generation/block_d const BlocksCatalog = preload("res://addons/block_code/code_generation/blocks_catalog.gd") var general_blocks: Dictionary +var block_script: BlockScriptSerialization func build_block_map(block_map: Dictionary, blocks: Array[BlockDefinition]): @@ -24,7 +25,9 @@ func free_block_map(block_map: Dictionary): func before_each(): - build_block_map(general_blocks, CategoryFactory.get_general_blocks()) + block_script = BlockScriptSerialization.new() + block_script.initialize() + build_block_map(general_blocks, block_script.get_available_blocks()) func after_each(): From 4118bae8efd175e7b8ae57fc167ff455db57222b Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 18 Sep 2024 14:49:57 -0700 Subject: [PATCH 02/13] BlocksCatalog: Remove by_class_name cache Instead, the list of blocks by class name is generated as needed inside get_blocks_by_class. --- .../code_generation/blocks_catalog.gd | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/addons/block_code/code_generation/blocks_catalog.gd b/addons/block_code/code_generation/blocks_catalog.gd index 6fd38e84..4bc8bb71 100644 --- a/addons/block_code/code_generation/blocks_catalog.gd +++ b/addons/block_code/code_generation/blocks_catalog.gd @@ -80,7 +80,6 @@ const _SETTINGS_FOR_CLASS_PROPERTY = { } static var _catalog: Dictionary -static var _by_class_name: Dictionary static func _setup_definitions_from_files(): @@ -91,9 +90,6 @@ static func _setup_definitions_from_files(): var target = block_definition.target_node_class if not target: continue - if not target in _by_class_name: - _by_class_name[target] = {} - _by_class_name[target][block_definition.name] = block_definition static func _add_property_definitions(_class_name: String, property_list: Array[Dictionary], property_settings: Dictionary): @@ -103,9 +99,6 @@ static func _add_property_definitions(_class_name: String, property_list: Array[ var block_settings = property_settings[property.name] var type_string: String = Types.VARIANT_TYPE_TO_STRING[property.type] - if not _class_name in _by_class_name: - _by_class_name[_class_name] = {} - # Setter var block_definition: BlockDefinition if block_settings.get("has_setter", true): @@ -124,7 +117,6 @@ static func _add_property_definitions(_class_name: String, property_list: Array[ ) ) _catalog[block_definition.name] = block_definition - _by_class_name[_class_name][block_definition.name] = block_definition # Changer if block_settings.get("has_change", true): @@ -143,7 +135,6 @@ static func _add_property_definitions(_class_name: String, property_list: Array[ ) ) _catalog[block_definition.name] = block_definition - _by_class_name[_class_name][block_definition.name] = block_definition # Getter block_definition = ( @@ -160,7 +151,6 @@ static func _add_property_definitions(_class_name: String, property_list: Array[ ) ) _catalog[block_definition.name] = block_definition - _by_class_name[_class_name][block_definition.name] = block_definition static func _get_inputmap_actions() -> Array[StringName]: @@ -234,11 +224,19 @@ static func has_block(block_name: StringName): return block_name in _catalog -static func get_blocks_by_class(_class_name: String): - if not _class_name in _by_class_name: - return [] - var block_definitions = _by_class_name[_class_name] as Dictionary - return block_definitions.values() +static func get_blocks_by_class(_class_name: String) -> Array[BlockDefinition]: + var result: Array[BlockDefinition] + + if not _class_name: + return result + + result.assign(_catalog.values().filter(_block_definition_has_class_name.bind(_class_name))) + + return result + + +static func _block_definition_has_class_name(block_definition: BlockDefinition, _class_name: String) -> bool: + return block_definition.target_node_class == _class_name static func _get_builtin_parents(_class_name: String) -> Array[String]: @@ -288,12 +286,8 @@ static func add_custom_blocks( ): setup() - if not _class_name in _by_class_name: - _by_class_name[_class_name] = {} - for block_definition in block_definitions: _catalog[block_definition.name] = block_definition - _by_class_name[_class_name][block_definition.name] = block_definition _add_property_definitions(_class_name, property_list, property_settings) From dc99328d28b94dafc2a835ed90e85f7c85d577b9 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 28 Aug 2024 17:58:10 -0700 Subject: [PATCH 03/13] BlockDefinition: Support extension scripts An extension script can implement functions which refine a block's presentation depending on the current context, such as the node it is attached to. https://phabricator.endlessm.com/T35564 --- addons/block_code/code_generation/block_definition.gd | 10 ++++++++++ addons/block_code/code_generation/block_extension.gd | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 addons/block_code/code_generation/block_extension.gd diff --git a/addons/block_code/code_generation/block_definition.gd b/addons/block_code/code_generation/block_definition.gd index 9f3bbb90..c5b6521f 100644 --- a/addons/block_code/code_generation/block_definition.gd +++ b/addons/block_code/code_generation/block_definition.gd @@ -27,6 +27,14 @@ const Types = preload("res://addons/block_code/types/types.gd") ## Empty except for blocks that have a defined scope @export var scope: String +@export var extension_script: GDScript + +var _extension: BlockExtension: + get: + if _extension == null and extension_script and extension_script.can_instantiate(): + _extension = extension_script.new() + return _extension as BlockExtension + func _init( p_name: StringName = &"", @@ -40,6 +48,7 @@ func _init( p_defaults = {}, p_signal_name: String = "", p_scope: String = "", + p_extension_script: GDScript = null, ): name = p_name target_node_class = p_target_node_class @@ -52,6 +61,7 @@ func _init( defaults = p_defaults signal_name = p_signal_name scope = p_scope + extension_script = p_extension_script func _to_string(): diff --git a/addons/block_code/code_generation/block_extension.gd b/addons/block_code/code_generation/block_extension.gd new file mode 100644 index 00000000..2a2fef97 --- /dev/null +++ b/addons/block_code/code_generation/block_extension.gd @@ -0,0 +1,3 @@ +@tool +class_name BlockExtension +extends Object From 3517792376dc1b79519f6b58a82a5074ceb5db68 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 28 Aug 2024 18:05:15 -0700 Subject: [PATCH 04/13] animationplayer_play: Add an extension script to generate an option list This extension customizes the block's "animation" options list using the list of animations in the block script's context node. https://phabricator.endlessm.com/T35564 --- .../blocks/graphics/animationplayer_play.gd | 15 +++++++++++++++ .../blocks/graphics/animationplayer_play.tres | 10 ++++++---- .../code_generation/block_definition.gd | 10 ++++++++++ .../block_code/code_generation/block_extension.gd | 4 ++++ 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 addons/block_code/blocks/graphics/animationplayer_play.gd diff --git a/addons/block_code/blocks/graphics/animationplayer_play.gd b/addons/block_code/blocks/graphics/animationplayer_play.gd new file mode 100644 index 00000000..63c603a2 --- /dev/null +++ b/addons/block_code/blocks/graphics/animationplayer_play.gd @@ -0,0 +1,15 @@ +@tool +extends BlockExtension + +const OptionData = preload("res://addons/block_code/code_generation/option_data.gd") + + +func get_defaults_for_node(context_node: Node) -> Dictionary: + var animation_player = context_node as AnimationPlayer + + if not animation_player: + return {} + + var animation_list = animation_player.get_animation_list() + + return {"animation": OptionData.new(animation_list)} diff --git a/addons/block_code/blocks/graphics/animationplayer_play.tres b/addons/block_code/blocks/graphics/animationplayer_play.tres index 2a2f373f..7868406a 100644 --- a/addons/block_code/blocks/graphics/animationplayer_play.tres +++ b/addons/block_code/blocks/graphics/animationplayer_play.tres @@ -1,7 +1,8 @@ -[gd_resource type="Resource" load_steps=4 format=3 uid="uid://c5e1byehtxwc0"] +[gd_resource type="Resource" load_steps=5 format=3 uid="uid://c5e1byehtxwc0"] [ext_resource type="Script" path="res://addons/block_code/code_generation/block_definition.gd" id="1_emeuv"] [ext_resource type="Script" path="res://addons/block_code/code_generation/option_data.gd" id="1_xu43h"] +[ext_resource type="Script" path="res://addons/block_code/blocks/graphics/animationplayer_play.gd" id="2_7ymgi"] [sub_resource type="Resource" id="Resource_vnp2w"] script = ExtResource("1_xu43h") @@ -16,14 +17,15 @@ description = "Play the animation." category = "Graphics | Animation" type = 2 variant_type = 0 -display_template = "Play {animation: STRING} {direction: OPTION}" +display_template = "Play {animation: OPTION} {direction: OPTION}" code_template = "if \"{direction}\" == \"ahead\": - play({animation}) + play(\"{animation}\") else: - play_backwards({animation}) + play_backwards(\"{animation}\") " defaults = { "direction": SubResource("Resource_vnp2w") } signal_name = "" scope = "" +extension_script = ExtResource("2_7ymgi") diff --git a/addons/block_code/code_generation/block_definition.gd b/addons/block_code/code_generation/block_definition.gd index c5b6521f..d1bb05a1 100644 --- a/addons/block_code/code_generation/block_definition.gd +++ b/addons/block_code/code_generation/block_definition.gd @@ -64,5 +64,15 @@ func _init( extension_script = p_extension_script +func get_defaults_for_node(parent_node: Node) -> Dictionary: + if not _extension: + return defaults + + # Use Dictionary.merge instead of Dictionary.merged for Godot 4.2 compatibility + var new_defaults := _extension.get_defaults_for_node(parent_node) + new_defaults.merge(defaults) + return new_defaults + + func _to_string(): return "%s - %s" % [name, target_node_class] diff --git a/addons/block_code/code_generation/block_extension.gd b/addons/block_code/code_generation/block_extension.gd index 2a2fef97..3e68a312 100644 --- a/addons/block_code/code_generation/block_extension.gd +++ b/addons/block_code/code_generation/block_extension.gd @@ -1,3 +1,7 @@ @tool class_name BlockExtension extends Object + + +func get_defaults_for_node(context_node: Node) -> Dictionary: + return {} From 81b645679afa9c0710167e6de358efae7c8e7f04 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Tue, 3 Sep 2024 18:00:04 -0700 Subject: [PATCH 05/13] Add TemplateEditor to generate UI based on a block format string This new component replaces StatementBlock.format_string in favour of a more declarative API. https://phabricator.endlessm.com/T35564 --- .../code_generation/block_definition.gd | 53 +++++++ .../ui/block_canvas/block_canvas.gd | 44 +++--- addons/block_code/ui/blocks/block/block.gd | 33 ++++- .../ui/blocks/control_block/control_block.gd | 20 +-- .../blocks/control_block/control_block.tscn | 11 +- .../ui/blocks/entry_block/entry_block.tscn | 12 +- .../blocks/parameter_block/parameter_block.gd | 17 +-- .../parameter_block/parameter_block.tscn | 18 +-- .../blocks/statement_block/statement_block.gd | 96 +------------ .../statement_block/statement_block.tscn | 12 +- .../parameter_input/parameter_input.gd | 55 ++++---- .../parameter_input/parameter_input.tscn | 27 ++-- .../parameter_output/parameter_output.gd | 9 +- .../parameter_output/parameter_output.tscn | 4 +- .../blocks/utilities/snap_point/snap_point.gd | 10 ++ .../template_editor/template_editor.gd | 132 ++++++++++++++++++ .../template_editor/template_editor.tscn | 17 +++ 17 files changed, 354 insertions(+), 216 deletions(-) create mode 100644 addons/block_code/ui/blocks/utilities/template_editor/template_editor.gd create mode 100644 addons/block_code/ui/blocks/utilities/template_editor/template_editor.tscn diff --git a/addons/block_code/code_generation/block_definition.gd b/addons/block_code/code_generation/block_definition.gd index d1bb05a1..fe36c001 100644 --- a/addons/block_code/code_generation/block_definition.gd +++ b/addons/block_code/code_generation/block_definition.gd @@ -4,6 +4,8 @@ extends Resource const Types = preload("res://addons/block_code/types/types.gd") +const FORMAT_STRING_PATTERN = "\\[(?[^\\]]+)\\]|\\{(?[^}]+)\\}|(?