Skip to content

Commit

Permalink
Context menu, delete, duplicate, and pin function
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DoomTas3r committed Nov 1, 2024
1 parent 2f66ee7 commit 8249281
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 22 deletions.
150 changes: 130 additions & 20 deletions addons/block_code/ui/blocks/block/block.gd
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,43 @@ signal modified
## Whether the block can be deleted by the Delete key.
var can_delete: bool = true

# FIXME: Variable pinned should be saved with the scene
## Whether the block is pinned
var pinned: bool:
set(value):
if not can_delete:
return

pinned = value

if pinned:
block_pinned_container = Container.new()
block_pinned_container.mouse_filter = Control.MOUSE_FILTER_IGNORE

var block_pinned_panel := Panel.new()
block_pinned_panel.custom_minimum_size = Vector2(16, 16)
block_pinned_panel.grow_horizontal = 2
block_pinned_panel.grow_vertical = 2
block_pinned_panel.self_modulate = Color(1, 1, 1, 0.75)

var block_pinned_icon := TextureRect.new()
block_pinned_icon.texture = EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons")

block_pinned_panel.add_child(block_pinned_icon)
block_pinned_container.add_child(block_pinned_panel)
add_child(block_pinned_container)
else:
remove_child(block_pinned_container)
block_pinned_container.queue_free()

block_pinned_container.visible = pinned

var block_pinned_container: Container

var _block_extension: BlockExtension

var _block_canvas: Node

@onready var _context := BlockEditorContext.get_default()


Expand Down Expand Up @@ -163,24 +198,62 @@ 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()
elif event.keycode == KEY_P:
accept_event()
pinned = not pinned
_pin_snapped_blocks(self, pinned)


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():
Expand All @@ -200,7 +273,8 @@ static func get_scene_path():


func _drag_started(offset: Vector2 = Vector2.ZERO):
drag_started.emit(self, offset)
if not pinned:
drag_started.emit(self, offset)


func disconnect_signals():
Expand Down Expand Up @@ -235,6 +309,42 @@ 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)


func _pin_snapped_blocks(node: Node, _is_pinned: bool):
for child in node.get_children():
if child is SnapPoint and child.has_snapped_block():
child.snapped_block.pinned = _is_pinned

if child is Container:
_pin_snapped_blocks(child, _is_pinned)
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -37,7 +39,30 @@ 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("Info", "EditorIcons"), "Summary")
_context_menu.add_icon_item(EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons"), "Unpin" if parent_block.pinned else "Pin")
_context_menu.add_separator()
_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

Expand All @@ -64,3 +89,33 @@ 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 == "Unpin" or _pressed_label == "Pin":
parent_block.pinned = not parent_block.pinned
parent_block._pin_snapped_blocks(parent_block, parent_block.pinned)
elif _pressed_label == "Summary":
# TODO: Replace tooltip with full summary
var _tooltip := parent_block._make_custom_tooltip(parent_block.get_tooltip())
var _tooltip_window := Popup.new()

_tooltip_window.position = DisplayServer.mouse_get_position()
_tooltip_window.popup_hide.connect(_cleanup)
_tooltip_window.add_child(_tooltip)
add_child(_tooltip_window)

_tooltip_window.show()
elif _pressed_label == "Delete":
parent_block.confirm_delete()


func _cleanup():
for child in get_children():
remove_child(child)
child.queue_free()

0 comments on commit 8249281

Please sign in to comment.