From b8013bf2407a6da1558743235d9727b946d24483 Mon Sep 17 00:00:00 2001 From: Daze Date: Thu, 24 Oct 2024 00:20:56 +0000 Subject: [PATCH] Add Duplicate blocks functionality Add a context menu with duplicate and delete options. Delete was previously only available from a Del shortcut. Duplicate affects snapped blocks. The context menu is displayed when the canvas receives a mouse right click. Fixes https://github.com/endlessm/godot-block-coding/issues/201 --- addons/block_code/ui/blocks/block/block.gd | 101 ++++++++++++++---- .../drag_drop_area/drag_drop_area.gd | 42 +++++++- 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/addons/block_code/ui/blocks/block/block.gd b/addons/block_code/ui/blocks/block/block.gd index d8ebc64b..fea24b3c 100644 --- a/addons/block_code/ui/blocks/block/block.gd +++ b/addons/block_code/ui/blocks/block/block.gd @@ -43,6 +43,8 @@ var can_delete: bool = true var _block_extension: BlockExtension +var _block_canvas: Node + @onready var _context := BlockEditorContext.get_default() @@ -163,24 +165,58 @@ func _on_block_extension_changed(): func _gui_input(event): if event is InputEventKey: - if event.pressed and event.keycode == KEY_DELETE: - # Always accept the Delete key so it doesn't propagate to the - # BlockCode node in the scene tree. - accept_event() - - if not can_delete: - return - - var dialog := ConfirmationDialog.new() - var num_blocks = _count_child_blocks(self) + 1 - # FIXME: Maybe this should use block_name or label, but that - # requires one to be both unique and human friendly. - if num_blocks > 1: - dialog.dialog_text = "Delete %d blocks?" % num_blocks - else: - dialog.dialog_text = "Delete block?" - dialog.confirmed.connect(remove_from_tree) - EditorInterface.popup_dialog_centered(dialog) + if event.pressed: + if event.keycode == KEY_DELETE: + # Always accept the Delete key so it doesn't propagate to the + # BlockCode node in the scene tree. + accept_event() + confirm_delete() + elif event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed and not event.meta_pressed: + # Should not accept when other keys are pressed + if event.keycode == KEY_D: + accept_event() + confirm_duplicate() + + +func confirm_delete(): + if not can_delete: + return + + var dialog := ConfirmationDialog.new() + var num_blocks = _count_child_blocks(self) + 1 + # FIXME: Maybe this should use block_name or label, but that + # requires one to be both unique and human friendly. + if num_blocks > 1: + dialog.dialog_text = "Delete %d blocks?" % num_blocks + else: + dialog.dialog_text = "Delete block?" + dialog.confirmed.connect(remove_from_tree) + EditorInterface.popup_dialog_centered(dialog) + + +func confirm_duplicate(): + if not can_delete: + return + + var new_block: Block = _context.block_script.instantiate_block(definition) + + var new_parent: Node = get_parent() + while not new_parent.name == "Window": + new_parent = new_parent.get_parent() + + if not _block_canvas: + _block_canvas = get_parent() + while not _block_canvas.name == "BlockCanvas": + _block_canvas = _block_canvas.get_parent() + + new_parent.add_child(new_block) + new_block.global_position = global_position + (Vector2(100, 50) * new_parent.scale) + + _copy_snapped_blocks(self, new_block) + + _block_canvas.reconnect_block.emit(new_block) + + modified.emit() func remove_from_tree(): @@ -236,6 +272,33 @@ func _count_child_blocks(node: Node) -> int: for child in node.get_children(): if child is SnapPoint and child.has_snapped_block(): count += 1 - count += _count_child_blocks(child) + + if child is Container: + count += _count_child_blocks(child) return count + + +func _copy_snapped_blocks(copy_from: Node, copy_to: Node): + var copy_to_child: Node + var child_index := 0 + var maximum_count := copy_to.get_child_count() + + for copy_from_child in copy_from.get_children(): + if child_index + 1 > maximum_count: + return + + copy_to_child = copy_to.get_child(child_index) + child_index += 1 + + if copy_from_child is SnapPoint and copy_from_child.has_snapped_block(): + copy_to_child.add_child(_context.block_script.instantiate_block(copy_from_child.snapped_block.definition)) + _block_canvas.reconnect_block.emit(copy_to_child.snapped_block) + elif copy_from_child.name.begins_with("ParameterInput"): + var raw_input = copy_from_child.get_raw_input() + + if not raw_input is Block: + copy_to_child.set_raw_input(raw_input) + + if copy_from_child is Container: + _copy_snapped_blocks(copy_from_child, copy_to_child) diff --git a/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd b/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd index 2269e901..7f386281 100644 --- a/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd +++ b/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd @@ -8,6 +8,7 @@ extends Control const Constants = preload("res://addons/block_code/ui/constants.gd") +const BlockTreeUtil = preload("res://addons/block_code/ui/block_tree_util.gd") signal drag_started(offset: Vector2) @@ -16,6 +17,7 @@ signal drag_started(offset: Vector2) @export var drag_outside: bool = false var _drag_start_position: Vector2 = Vector2.INF +var parent_block: Block func _gui_input(event: InputEvent) -> void: @@ -27,7 +29,7 @@ func _gui_input(event: InputEvent) -> void: var button_event: InputEventMouseButton = event as InputEventMouseButton - if button_event.button_index != MOUSE_BUTTON_LEFT: + if button_event.button_index != MOUSE_BUTTON_LEFT and button_event.button_index != MOUSE_BUTTON_RIGHT: return if button_event.double_click: @@ -37,7 +39,27 @@ func _gui_input(event: InputEvent) -> void: elif button_event.pressed: # Keep track of where the mouse click originated, but allow this # event to propagate to other nodes. - _drag_start_position = event.global_position + if button_event.button_index == MOUSE_BUTTON_LEFT: + _drag_start_position = event.global_position + else: + if not parent_block: + parent_block = BlockTreeUtil.get_parent_block(self) + + if parent_block and parent_block.can_delete: + # Accepts to avoid menu conflicts + accept_event() + + # A new right-click menu with items + var _context_menu := PopupMenu.new() + _context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Duplicate", "EditorIcons"), "Duplicate") + _context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Remove", "EditorIcons"), "Delete") + _context_menu.popup_hide.connect(_cleanup) + _context_menu.id_pressed.connect(_menu_pressed.bind(_context_menu)) + + _context_menu.position = DisplayServer.mouse_get_position() + add_child(_context_menu) + + _context_menu.show() else: _drag_start_position = Vector2.INF @@ -64,3 +86,19 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() drag_started.emit(_drag_start_position - motion_event.global_position) _drag_start_position = Vector2.INF + + +func _menu_pressed(_index: int, _context_menu: PopupMenu): + # Getting which item was pressed and the corresponding function + var _pressed_label: String = _context_menu.get_item_text(_index) + + if _pressed_label == "Duplicate": + parent_block.confirm_duplicate() + elif _pressed_label == "Delete": + parent_block.confirm_delete() + + +func _cleanup(): + for child in get_children(): + remove_child(child) + child.queue_free()