Skip to content

Commit fea517e

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 fea517e

File tree

2 files changed

+190
-22
lines changed

2 files changed

+190
-22
lines changed

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

Lines changed: 127 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,42 @@ 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 not block_pinned_container:
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+
70+
block_pinned_container.visible = pinned
71+
72+
var block_pinned_container: Container
73+
4474
var _block_extension: BlockExtension
4575

76+
var _block_canvas: Node
77+
4678
@onready var _context := BlockEditorContext.get_default()
79+
@onready var _icon_pin := EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons")
4780

4881

4982
func _ready():
@@ -163,24 +196,62 @@ func _on_block_extension_changed():
163196

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

185256

186257
func remove_from_tree():
@@ -200,7 +271,8 @@ static func get_scene_path():
200271

201272

202273
func _drag_started(offset: Vector2 = Vector2.ZERO):
203-
drag_started.emit(self, offset)
274+
if not pinned:
275+
drag_started.emit(self, offset)
204276

205277

206278
func disconnect_signals():
@@ -235,6 +307,41 @@ func _count_child_blocks(node: Node) -> int:
235307
for child in node.get_children():
236308
if child is SnapPoint and child.has_snapped_block():
237309
count += 1
238-
count += _count_child_blocks(child)
310+
311+
if child is Container:
312+
count += _count_child_blocks(child)
239313

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

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

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
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")
12+
13+
@onready var _icon_duplicate := EditorInterface.get_editor_theme().get_icon("Duplicate", "EditorIcons")
14+
@onready var _icon_pin := EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons")
15+
@onready var _icon_info := EditorInterface.get_editor_theme().get_icon("Info", "EditorIcons")
16+
@onready var _icon_remove := EditorInterface.get_editor_theme().get_icon("Remove", "EditorIcons")
1117

1218
signal drag_started(offset: Vector2)
1319

@@ -16,6 +22,11 @@ signal drag_started(offset: Vector2)
1622
@export var drag_outside: bool = false
1723

1824
var _drag_start_position: Vector2 = Vector2.INF
25+
var parent_block: Block
26+
27+
28+
func _ready() -> void:
29+
parent_block = BlockTreeUtil.get_parent_block(self)
1930

2031

2132
func _gui_input(event: InputEvent) -> void:
@@ -27,7 +38,7 @@ func _gui_input(event: InputEvent) -> void:
2738

2839
var button_event: InputEventMouseButton = event as InputEventMouseButton
2940

30-
if button_event.button_index != MOUSE_BUTTON_LEFT:
41+
if button_event.button_index != MOUSE_BUTTON_LEFT and button_event.button_index != MOUSE_BUTTON_RIGHT:
3142
return
3243

3344
if button_event.double_click:
@@ -37,7 +48,27 @@ func _gui_input(event: InputEvent) -> void:
3748
elif button_event.pressed:
3849
# Keep track of where the mouse click originated, but allow this
3950
# event to propagate to other nodes.
40-
_drag_start_position = event.global_position
51+
if button_event.button_index == MOUSE_BUTTON_LEFT:
52+
_drag_start_position = event.global_position
53+
else:
54+
if parent_block and parent_block.can_delete:
55+
# Accepts to avoid menu conflicts
56+
accept_event()
57+
58+
# A new right-click menu with items
59+
var _context_menu := PopupMenu.new()
60+
_context_menu.add_icon_item(_icon_duplicate, "Duplicate")
61+
_context_menu.add_icon_item(_icon_info, "Summary")
62+
_context_menu.add_icon_item(_icon_pin, "Unpin" if parent_block.pinned else "Pin")
63+
_context_menu.add_separator()
64+
_context_menu.add_icon_item(_icon_remove, "Delete")
65+
_context_menu.popup_hide.connect(_cleanup)
66+
_context_menu.id_pressed.connect(_menu_pressed.bind(_context_menu))
67+
68+
_context_menu.position = get_global_mouse_position()
69+
add_child(_context_menu)
70+
71+
_context_menu.show()
4172
else:
4273
_drag_start_position = Vector2.INF
4374

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

0 commit comments

Comments
 (0)