Skip to content

Commit a5249a0

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 a5249a0

File tree

2 files changed

+188
-22
lines changed

2 files changed

+188
-22
lines changed

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

+131-20
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,45 @@ 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 = _icon_pin
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()
82+
@onready var _icon_pin := EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons")
4783

4884

4985
func _ready():
@@ -163,24 +199,62 @@ func _on_block_extension_changed():
163199

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

185259

186260
func remove_from_tree():
@@ -200,7 +274,8 @@ static func get_scene_path():
200274

201275

202276
func _drag_started(offset: Vector2 = Vector2.ZERO):
203-
drag_started.emit(self, offset)
277+
if not pinned:
278+
drag_started.emit(self, offset)
204279

205280

206281
func disconnect_signals():
@@ -235,6 +310,42 @@ func _count_child_blocks(node: Node) -> int:
235310
for child in node.get_children():
236311
if child is SnapPoint and child.has_snapped_block():
237312
count += 1
238-
count += _count_child_blocks(child)
313+
314+
if child is Container:
315+
count += _count_child_blocks(child)
239316

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