Skip to content

Commit b8013bf

Browse files
DoomTas3rmanuq
authored andcommitted
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 #201
1 parent d35b91e commit b8013bf

File tree

2 files changed

+122
-21
lines changed

2 files changed

+122
-21
lines changed

addons/block_code/ui/blocks/block/block.gd

+82-19
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ var can_delete: bool = true
4343

4444
var _block_extension: BlockExtension
4545

46+
var _block_canvas: Node
47+
4648
@onready var _context := BlockEditorContext.get_default()
4749

4850

@@ -163,24 +165,58 @@ func _on_block_extension_changed():
163165

164166
func _gui_input(event):
165167
if event is InputEventKey:
166-
if event.pressed and event.keycode == KEY_DELETE:
167-
# Always accept the Delete key so it doesn't propagate to the
168-
# BlockCode node in the scene tree.
169-
accept_event()
170-
171-
if not can_delete:
172-
return
173-
174-
var dialog := ConfirmationDialog.new()
175-
var num_blocks = _count_child_blocks(self) + 1
176-
# FIXME: Maybe this should use block_name or label, but that
177-
# requires one to be both unique and human friendly.
178-
if num_blocks > 1:
179-
dialog.dialog_text = "Delete %d blocks?" % num_blocks
180-
else:
181-
dialog.dialog_text = "Delete block?"
182-
dialog.confirmed.connect(remove_from_tree)
183-
EditorInterface.popup_dialog_centered(dialog)
168+
if event.pressed:
169+
if event.keycode == KEY_DELETE:
170+
# Always accept the Delete key so it doesn't propagate to the
171+
# BlockCode node in the scene tree.
172+
accept_event()
173+
confirm_delete()
174+
elif event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed and not event.meta_pressed:
175+
# Should not accept when other keys are pressed
176+
if event.keycode == KEY_D:
177+
accept_event()
178+
confirm_duplicate()
179+
180+
181+
func confirm_delete():
182+
if not can_delete:
183+
return
184+
185+
var dialog := ConfirmationDialog.new()
186+
var num_blocks = _count_child_blocks(self) + 1
187+
# FIXME: Maybe this should use block_name or label, but that
188+
# requires one to be both unique and human friendly.
189+
if num_blocks > 1:
190+
dialog.dialog_text = "Delete %d blocks?" % num_blocks
191+
else:
192+
dialog.dialog_text = "Delete block?"
193+
dialog.confirmed.connect(remove_from_tree)
194+
EditorInterface.popup_dialog_centered(dialog)
195+
196+
197+
func confirm_duplicate():
198+
if not can_delete:
199+
return
200+
201+
var new_block: Block = _context.block_script.instantiate_block(definition)
202+
203+
var new_parent: Node = get_parent()
204+
while not new_parent.name == "Window":
205+
new_parent = new_parent.get_parent()
206+
207+
if not _block_canvas:
208+
_block_canvas = get_parent()
209+
while not _block_canvas.name == "BlockCanvas":
210+
_block_canvas = _block_canvas.get_parent()
211+
212+
new_parent.add_child(new_block)
213+
new_block.global_position = global_position + (Vector2(100, 50) * new_parent.scale)
214+
215+
_copy_snapped_blocks(self, new_block)
216+
217+
_block_canvas.reconnect_block.emit(new_block)
218+
219+
modified.emit()
184220

185221

186222
func remove_from_tree():
@@ -236,6 +272,33 @@ func _count_child_blocks(node: Node) -> int:
236272
for child in node.get_children():
237273
if child is SnapPoint and child.has_snapped_block():
238274
count += 1
239-
count += _count_child_blocks(child)
275+
276+
if child is Container:
277+
count += _count_child_blocks(child)
240278

241279
return count
280+
281+
282+
func _copy_snapped_blocks(copy_from: Node, copy_to: Node):
283+
var copy_to_child: Node
284+
var child_index := 0
285+
var maximum_count := copy_to.get_child_count()
286+
287+
for copy_from_child in copy_from.get_children():
288+
if child_index + 1 > maximum_count:
289+
return
290+
291+
copy_to_child = copy_to.get_child(child_index)
292+
child_index += 1
293+
294+
if copy_from_child is SnapPoint and copy_from_child.has_snapped_block():
295+
copy_to_child.add_child(_context.block_script.instantiate_block(copy_from_child.snapped_block.definition))
296+
_block_canvas.reconnect_block.emit(copy_to_child.snapped_block)
297+
elif copy_from_child.name.begins_with("ParameterInput"):
298+
var raw_input = copy_from_child.get_raw_input()
299+
300+
if not raw_input is Block:
301+
copy_to_child.set_raw_input(raw_input)
302+
303+
if copy_from_child is Container:
304+
_copy_snapped_blocks(copy_from_child, copy_to_child)

addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd

+40-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
extends Control
99

1010
const Constants = preload("res://addons/block_code/ui/constants.gd")
11+
const BlockTreeUtil = preload("res://addons/block_code/ui/block_tree_util.gd")
1112

1213
signal drag_started(offset: Vector2)
1314

@@ -16,6 +17,7 @@ signal drag_started(offset: Vector2)
1617
@export var drag_outside: bool = false
1718

1819
var _drag_start_position: Vector2 = Vector2.INF
20+
var parent_block: Block
1921

2022

2123
func _gui_input(event: InputEvent) -> void:
@@ -27,7 +29,7 @@ func _gui_input(event: InputEvent) -> void:
2729

2830
var button_event: InputEventMouseButton = event as InputEventMouseButton
2931

30-
if button_event.button_index != MOUSE_BUTTON_LEFT:
32+
if button_event.button_index != MOUSE_BUTTON_LEFT and button_event.button_index != MOUSE_BUTTON_RIGHT:
3133
return
3234

3335
if button_event.double_click:
@@ -37,7 +39,27 @@ func _gui_input(event: InputEvent) -> void:
3739
elif button_event.pressed:
3840
# Keep track of where the mouse click originated, but allow this
3941
# event to propagate to other nodes.
40-
_drag_start_position = event.global_position
42+
if button_event.button_index == MOUSE_BUTTON_LEFT:
43+
_drag_start_position = event.global_position
44+
else:
45+
if not parent_block:
46+
parent_block = BlockTreeUtil.get_parent_block(self)
47+
48+
if parent_block and parent_block.can_delete:
49+
# Accepts to avoid menu conflicts
50+
accept_event()
51+
52+
# A new right-click menu with items
53+
var _context_menu := PopupMenu.new()
54+
_context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Duplicate", "EditorIcons"), "Duplicate")
55+
_context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Remove", "EditorIcons"), "Delete")
56+
_context_menu.popup_hide.connect(_cleanup)
57+
_context_menu.id_pressed.connect(_menu_pressed.bind(_context_menu))
58+
59+
_context_menu.position = DisplayServer.mouse_get_position()
60+
add_child(_context_menu)
61+
62+
_context_menu.show()
4163
else:
4264
_drag_start_position = Vector2.INF
4365

@@ -64,3 +86,19 @@ func _input(event: InputEvent) -> void:
6486
get_viewport().set_input_as_handled()
6587
drag_started.emit(_drag_start_position - motion_event.global_position)
6688
_drag_start_position = Vector2.INF
89+
90+
91+
func _menu_pressed(_index: int, _context_menu: PopupMenu):
92+
# Getting which item was pressed and the corresponding function
93+
var _pressed_label: String = _context_menu.get_item_text(_index)
94+
95+
if _pressed_label == "Duplicate":
96+
parent_block.confirm_duplicate()
97+
elif _pressed_label == "Delete":
98+
parent_block.confirm_delete()
99+
100+
101+
func _cleanup():
102+
for child in get_children():
103+
remove_child(child)
104+
child.queue_free()

0 commit comments

Comments
 (0)