diff --git a/addons/block_code/ui/blocks/block/block.gd b/addons/block_code/ui/blocks/block/block.gd index 38123c5c..be3d2bf0 100644 --- a/addons/block_code/ui/blocks/block/block.gd +++ b/addons/block_code/ui/blocks/block/block.gd @@ -41,9 +41,45 @@ 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 = _icon_pin + + 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() +@onready var _icon_pin := EditorInterface.get_editor_theme().get_icon("Pin", "EditorIcons") func _ready(): @@ -163,24 +199,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(): @@ -200,7 +274,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(): @@ -235,6 +310,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(copy_from_child.get_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) diff --git a/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd b/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd index 2269e901..c0eafa10 100644 --- a/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd +++ b/addons/block_code/ui/blocks/utilities/drag_drop_area/drag_drop_area.gd @@ -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) @@ -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: @@ -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: @@ -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 @@ -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()