Skip to content

Commit 8249281

Browse files
committed
Context menu, delete, duplicate, and pin function
Adds a context menu with duplicate, summary, pin, and delete functions with shortcuts ctrl + d to duplicate, ctrl + p to pin. Duplicate and pin affects snapped blocks. Context menu displays when drag drop area receives a right click
1 parent 2f66ee7 commit 8249281

File tree

2 files changed

+187
-22
lines changed

2 files changed

+187
-22
lines changed

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

+130-20
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,43 @@ signal modified
4141
## Whether the block can be deleted by the Delete key.
4242
var can_delete: bool = true
4343

44+
# FIXME: Variable pinned should be saved with the scene
45+
## Whether the block is pinned
46+
var pinned: bool:
47+
set(value):
48+
if not can_delete:
49+
return
50+
51+
pinned = value
52+
53+
if pinned:
54+
block_pinned_container = Container.new()
55+
block_pinned_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
56+
57+
var block_pinned_panel := Panel.new()
58+
block_pinned_panel.custom_minimum_size = Vector2(16, 16)
59+
block_pinned_panel.grow_horizontal = 2
60+
block_pinned_panel.grow_vertical = 2
61+
block_pinned_panel.self_modulate = Color(1, 1, 1, 0.75)
62+
63+
var block_pinned_icon := TextureRect.new()
64+
block_pinned_icon.texture = EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons")
65+
66+
block_pinned_panel.add_child(block_pinned_icon)
67+
block_pinned_container.add_child(block_pinned_panel)
68+
add_child(block_pinned_container)
69+
else:
70+
remove_child(block_pinned_container)
71+
block_pinned_container.queue_free()
72+
73+
block_pinned_container.visible = pinned
74+
75+
var block_pinned_container: Container
76+
4477
var _block_extension: BlockExtension
4578

79+
var _block_canvas: Node
80+
4681
@onready var _context := BlockEditorContext.get_default()
4782

4883

@@ -163,24 +198,62 @@ func _on_block_extension_changed():
163198

164199
func _gui_input(event):
165200
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)
201+
if event.pressed:
202+
if event.keycode == KEY_DELETE:
203+
# Always accept the Delete key so it doesn't propagate to the
204+
# BlockCode node in the scene tree.
205+
accept_event()
206+
confirm_delete()
207+
elif event.ctrl_pressed and not event.shift_pressed and not event.alt_pressed and not event.meta_pressed:
208+
# Should not accept when other keys are pressed
209+
if event.keycode == KEY_D:
210+
accept_event()
211+
confirm_duplicate()
212+
elif event.keycode == KEY_P:
213+
accept_event()
214+
pinned = not pinned
215+
_pin_snapped_blocks(self, pinned)
216+
217+
218+
func confirm_delete():
219+
if not can_delete:
220+
return
221+
222+
var dialog := ConfirmationDialog.new()
223+
var num_blocks = _count_child_blocks(self) + 1
224+
# FIXME: Maybe this should use block_name or label, but that
225+
# requires one to be both unique and human friendly.
226+
if num_blocks > 1:
227+
dialog.dialog_text = "Delete %d blocks?" % num_blocks
228+
else:
229+
dialog.dialog_text = "Delete block?"
230+
dialog.confirmed.connect(remove_from_tree)
231+
EditorInterface.popup_dialog_centered(dialog)
232+
233+
234+
func confirm_duplicate():
235+
if not can_delete:
236+
return
237+
238+
var new_block: Block = _context.block_script.instantiate_block(definition)
239+
240+
var new_parent: Node = get_parent()
241+
while not new_parent.name == "Window":
242+
new_parent = new_parent.get_parent()
243+
244+
if not _block_canvas:
245+
_block_canvas = get_parent()
246+
while not _block_canvas.name == "BlockCanvas":
247+
_block_canvas = _block_canvas.get_parent()
248+
249+
new_parent.add_child(new_block)
250+
new_block.global_position = global_position + (Vector2(100, 50) * new_parent.scale)
251+
252+
_copy_snapped_blocks(self, new_block)
253+
254+
_block_canvas.reconnect_block.emit(new_block)
255+
256+
modified.emit()
184257

185258

186259
func remove_from_tree():
@@ -200,7 +273,8 @@ static func get_scene_path():
200273

201274

202275
func _drag_started(offset: Vector2 = Vector2.ZERO):
203-
drag_started.emit(self, offset)
276+
if not pinned:
277+
drag_started.emit(self, offset)
204278

205279

206280
func disconnect_signals():
@@ -235,6 +309,42 @@ func _count_child_blocks(node: Node) -> int:
235309
for child in node.get_children():
236310
if child is SnapPoint and child.has_snapped_block():
237311
count += 1
238-
count += _count_child_blocks(child)
312+
313+
if child is Container:
314+
count += _count_child_blocks(child)
239315

240316
return count
317+
318+
319+
func _copy_snapped_blocks(copy_from: Node, copy_to: Node):
320+
var copy_to_child: Node
321+
var child_index := 0
322+
var maximum_count := copy_to.get_child_count()
323+
324+
for copy_from_child in copy_from.get_children():
325+
if child_index + 1 > maximum_count:
326+
return
327+
328+
copy_to_child = copy_to.get_child(child_index)
329+
child_index += 1
330+
331+
if copy_from_child is SnapPoint and copy_from_child.has_snapped_block():
332+
copy_to_child.add_child(_context.block_script.instantiate_block(copy_from_child.snapped_block.definition))
333+
_block_canvas.reconnect_block.emit(copy_to_child.snapped_block)
334+
elif copy_from_child.name.begins_with("ParameterInput"):
335+
var raw_input = copy_from_child.get_raw_input()
336+
337+
if not raw_input is Block:
338+
copy_to_child.set_raw_input(raw_input)
339+
340+
if copy_from_child is Container:
341+
_copy_snapped_blocks(copy_from_child, copy_to_child)
342+
343+
344+
func _pin_snapped_blocks(node: Node, _is_pinned: bool):
345+
for child in node.get_children():
346+
if child is SnapPoint and child.has_snapped_block():
347+
child.snapped_block.pinned = _is_pinned
348+
349+
if child is Container:
350+
_pin_snapped_blocks(child, _is_pinned)

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

+57-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,30 @@ 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("Info", "EditorIcons"), "Summary")
56+
_context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons"), "Unpin" if parent_block.pinned else "Pin")
57+
_context_menu.add_separator()
58+
_context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Remove", "EditorIcons"), "Delete")
59+
_context_menu.popup_hide.connect(_cleanup)
60+
_context_menu.id_pressed.connect(_menu_pressed.bind(_context_menu))
61+
62+
_context_menu.position = DisplayServer.mouse_get_position()
63+
add_child(_context_menu)
64+
65+
_context_menu.show()
4166
else:
4267
_drag_start_position = Vector2.INF
4368

@@ -64,3 +89,33 @@ func _input(event: InputEvent) -> void:
6489
get_viewport().set_input_as_handled()
6590
drag_started.emit(_drag_start_position - motion_event.global_position)
6691
_drag_start_position = Vector2.INF
92+
93+
94+
func _menu_pressed(_index: int, _context_menu: PopupMenu):
95+
# Getting which item was pressed and the corresponding function
96+
var _pressed_label: String = _context_menu.get_item_text(_index)
97+
98+
if _pressed_label == "Duplicate":
99+
parent_block.confirm_duplicate()
100+
elif _pressed_label == "Unpin" or _pressed_label == "Pin":
101+
parent_block.pinned = not parent_block.pinned
102+
parent_block._pin_snapped_blocks(parent_block, parent_block.pinned)
103+
elif _pressed_label == "Summary":
104+
# TODO: Replace tooltip with full summary
105+
var _tooltip := parent_block._make_custom_tooltip(parent_block.get_tooltip())
106+
var _tooltip_window := Popup.new()
107+
108+
_tooltip_window.position = DisplayServer.mouse_get_position()
109+
_tooltip_window.popup_hide.connect(_cleanup)
110+
_tooltip_window.add_child(_tooltip)
111+
add_child(_tooltip_window)
112+
113+
_tooltip_window.show()
114+
elif _pressed_label == "Delete":
115+
parent_block.confirm_delete()
116+
117+
118+
func _cleanup():
119+
for child in get_children():
120+
remove_child(child)
121+
child.queue_free()

0 commit comments

Comments
 (0)